diff --git a/src/assets/pos.css b/src/assets/pos.css index a117ca8c..a3f15720 100644 --- a/src/assets/pos.css +++ b/src/assets/pos.css @@ -760,6 +760,7 @@ .pos-step-one-insights { display: grid; + align-items: stretch; gap: 0.9rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr)); } @@ -903,6 +904,7 @@ .pos-step-one-insight-card__section { display: flex; + flex: 1 1 auto; flex-direction: column; gap: 0.55rem; min-width: 0; @@ -970,6 +972,7 @@ .pos-step-one-empty-state, .pos-step-one-loading-state { display: flex; + flex: 1 1 auto; align-items: center; justify-content: center; min-height: 4rem; diff --git a/src/components/displays/department/pos/steps/PosDepartmentStep1.vue b/src/components/displays/department/pos/steps/PosDepartmentStep1.vue index e5be63cd..14bd4de6 100644 --- a/src/components/displays/department/pos/steps/PosDepartmentStep1.vue +++ b/src/components/displays/department/pos/steps/PosDepartmentStep1.vue @@ -660,7 +660,9 @@ const handleDesktopLastWashCopy = async (payload = {}) => { return; } - const didCopy = await copyLastWashItemsToCurrentOrder(payload?.items || []); + const didCopy = await copyLastWashItemsToCurrentOrder(payload?.items || [], { + sourceReference: payload?.order?.reference, + }); if (!didCopy) { return; } diff --git a/src/components/displays/department/pos/steps/elements/PosDesktopLastWashCard.vue b/src/components/displays/department/pos/steps/elements/PosDesktopLastWashCard.vue index 45457198..51b4940c 100644 --- a/src/components/displays/department/pos/steps/elements/PosDesktopLastWashCard.vue +++ b/src/components/displays/department/pos/steps/elements/PosDesktopLastWashCard.vue @@ -190,7 +190,7 @@ watch(normalizedOrderId, (nextOrderId, previousOrderId) => { -
+

Indhold

diff --git a/src/components/displays/department/pos/steps/elements/PosDesktopVehicleSummaryCard.vue b/src/components/displays/department/pos/steps/elements/PosDesktopVehicleSummaryCard.vue index 696786e9..c6120d5a 100644 --- a/src/components/displays/department/pos/steps/elements/PosDesktopVehicleSummaryCard.vue +++ b/src/components/displays/department/pos/steps/elements/PosDesktopVehicleSummaryCard.vue @@ -248,7 +248,7 @@ const hasVehicleSummaryRows = computed(() => vehicleSummaryRows.value.length > 0 -
+

Ydelse og tilvalg

diff --git a/src/components/release/ReleaseChannelSelector.vue b/src/components/release/ReleaseChannelSelector.vue index 5f75c0cc..36009851 100644 --- a/src/components/release/ReleaseChannelSelector.vue +++ b/src/components/release/ReleaseChannelSelector.vue @@ -4,7 +4,6 @@ import { useI18n } from "vue-i18n"; import { releaseChannelKey, releaseChannelOptions, - selectedReleaseChannelSlug, } from "@/services/releaseChannelAvailability.js"; import { releaseRuntimeState } from "@/services/releaseTimeline.js"; @@ -31,7 +30,7 @@ const tr = (key, fallback, params = {}) => { return te(path) ? t(path, params) : fallback; }; -const activeSlug = computed(() => selectedReleaseChannelSlug.value || releaseChannelKey(releaseRuntimeState.channel)); +const activeSlug = computed(() => releaseChannelKey(releaseRuntimeState.channel)); const isSidebar = computed(() => props.variant === "sidebar"); const selectorTitle = computed(() => isSidebar.value ? tr("sidebar_title", "Release") : tr("title", "Release channel") diff --git a/src/components/release/ReleaseChannelSidebarSelector.vue b/src/components/release/ReleaseChannelSidebarSelector.vue index 65e49f2e..faa00693 100644 --- a/src/components/release/ReleaseChannelSidebarSelector.vue +++ b/src/components/release/ReleaseChannelSidebarSelector.vue @@ -1,23 +1,32 @@ + + diff --git a/src/components/release/ReleaseChannelUnavailable.vue b/src/components/release/ReleaseChannelUnavailable.vue index ff4b9657..03a067dd 100644 --- a/src/components/release/ReleaseChannelUnavailable.vue +++ b/src/components/release/ReleaseChannelUnavailable.vue @@ -9,7 +9,7 @@ import { releaseChannelSelectorVisible, releaseChannelUnavailableStatus, RELEASE_CHANNEL_CHECK_INTERVAL_MS, - selectReleaseChannel, + switchSelectedReleaseChannel, } from "@/services/releaseChannelAvailability.js"; const router = useRouter(); @@ -71,15 +71,14 @@ const ignoreForFiveMinutes = () => { }; const switchChannel = async (option) => { - const slug = selectReleaseChannel(option.channel); - if (!slug || switchingSlug.value) { + if (switchingSlug.value) { return; } - switchingSlug.value = slug; + switchingSlug.value = option.channelSlug || option.channel?.slug || ""; checkError.value = ""; try { - await SessionUser.refreshReleaseRuntime(); + await switchSelectedReleaseChannel(option.channel, SessionUser.refreshReleaseRuntime); } catch (error) { checkError.value = tr("refresh_error"); } finally { diff --git a/src/components/session/token/SessionUser.vue b/src/components/session/token/SessionUser.vue index 56e929ff..430c74fa 100644 --- a/src/components/session/token/SessionUser.vue +++ b/src/components/session/token/SessionUser.vue @@ -128,14 +128,18 @@ const applyReleaseRuntimeConfig = (runtime) => { }; }; -export const refreshReleaseRuntime = async () => { - return fetchReleaseRuntime() - .then((runtime) => { - applyReleaseRuntimeConfig(runtime || {}); - }) - .catch((error) => { - console.warn("Could not refresh release runtime", error); - }); +export const refreshReleaseRuntime = async ({ throwOnError = false } = {}) => { + try { + const runtime = await fetchReleaseRuntime(); + applyReleaseRuntimeConfig(runtime || {}); + return runtime || {}; + } catch (error) { + console.warn("Could not refresh release runtime", error); + if (throwOnError) { + throw error; + } + return null; + } }; /** diff --git a/src/components/shop/POSDepartmentProcess.vue b/src/components/shop/POSDepartmentProcess.vue index 5b8fa94c..8050802b 100644 --- a/src/components/shop/POSDepartmentProcess.vue +++ b/src/components/shop/POSDepartmentProcess.vue @@ -1234,6 +1234,20 @@ const getOrderItemNotes = (item) => { return notesValue === "" ? null : notesValue; }; +const copyLastWashReferenceToEmptyCurrentOrder = async (sourceReference) => { + const normalizedSourceReference = String(sourceReference ?? "").trim(); + if (!normalizedSourceReference || !isBlankPosMetadataValue(reference.value)) { + return; + } + + reference.value = normalizedSourceReference; + + const normalizedOrderId = toPositiveInteger(order_id.value); + if (normalizedOrderId) { + await SessionUser.objects.orders.set.reference(normalizedOrderId, normalizedSourceReference); + } +}; + const createCopiedOrderItem = (targetOrderId, sourceItem, relatedItemId = null) => { const productId = getOrderItemProductId(sourceItem); const quantity = getOrderItemQuantity(sourceItem); @@ -1262,6 +1276,8 @@ export const copyLastWashItemsToCurrentOrder = async (sourceItems = [], options return false; } + await copyLastWashReferenceToEmptyCurrentOrder(options.sourceReference ?? options.sourceOrder?.reference); + const didEnsureOrder = await createOrder({ ...options, isMobile: false, diff --git a/src/i18n/generated/da-v2.json b/src/i18n/generated/da-v2.json index 03ac667e..aa2ecaee 100644 --- a/src/i18n/generated/da-v2.json +++ b/src/i18n/generated/da-v2.json @@ -2516,6 +2516,21 @@ "add_suggested_channel": "Tilføj foreslået kanal", "module_health_empty": "Modul-health snapshots vises, når probes er blevet registreret." }, + "status": { + "confirm_issue_action": "Koer denne Release Manager-handling? Den kan aendre deployment-tilstand og bliver auditeret.", + "choose_bundle_prompt": "Indtast bundle-id'et, der skal saettes for denne kanal.", + "impact": "Konsekvens", + "cause": "Aarsag", + "automated_fix": "Automatisk rettelse", + "manual_fallback": "Manuel fallback", + "related_deployment": "Relateret deployment", + "recent_result": "Seneste resultat", + "no_automated_action": "Ingen automatisk handling tilgaengelig.", + "deployment": "Deployment", + "target": "Maal", + "coolify_target": "Coolify-maal", + "service_set": "Service set" + }, "settings": { "title": "Release Manager-indstillinger", "subtitle": "Konfigurer GitHub-tokenet, der bruges til private repositories og branch-opslag.", @@ -2616,7 +2631,8 @@ "release_runtime": "Release-runtime", "bundle": "Bundle", "frontend": "Frontend", - "api": "API" + "api": "API", + "switch_error": "Release-kanalen kunne ikke skiftes. Den forrige kanal er stadig aktiv." }, "common": { "yes": "ja", @@ -2628,6 +2644,7 @@ "rollback": "Rul tilbage", "update": "Opdater", "create": "Opret", + "publish_release": "Publicer release", "clear": "Ryd", "assign": "Tildel", "remove": "Fjern", @@ -2880,6 +2897,19 @@ "ssl_domain_message": "Skal være et DNS-domæne, der routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io", "ssl_domain_placeholder": "Load balancer-domæne", "https_domain_for_coolify": "Domæne kontrolleret af load balanceren", + "endpoint_mode": "Endpoint mode", + "endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.", + "endpoint_mode_auto": "Auto", + "endpoint_mode_manual": "Manual", + "manual_endpoint_host": "Manual host", + "manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.", + "manual_endpoint_host_placeholder": "api-v2.truckwash.io", + "manual_endpoint_port": "Manual public port", + "manual_endpoint_port_message": "Optional public port from 1 to 65535.", + "app_port": "App port", + "app_port_message": "Internal container port exposed to Coolify routing.", + "auto_gateway_endpoint": "Automatic gateway endpoint", + "pending_automatic_endpoint": "Automatic endpoint resolution pending", "auto_deploy": "Auto deploy", "auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.", "coolify_ssl": "Coolify SSL", diff --git a/src/i18n/generated/de-v2.json b/src/i18n/generated/de-v2.json index 5f429b18..01c9fe8a 100644 --- a/src/i18n/generated/de-v2.json +++ b/src/i18n/generated/de-v2.json @@ -2990,6 +2990,19 @@ "ssl_domain_message": "Muss eine DNS-Domain sein, die zum Coolify Load Balancer geroutet wird. Beispiel: api-v2.truckwash.io", "ssl_domain_placeholder": "Load-Balancer-Domain", "https_domain_for_coolify": "Domain unter Kontrolle des Load Balancers", + "endpoint_mode": "Endpoint mode", + "endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.", + "endpoint_mode_auto": "Auto", + "endpoint_mode_manual": "Manual", + "manual_endpoint_host": "Manual host", + "manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.", + "manual_endpoint_host_placeholder": "api-v2.truckwash.io", + "manual_endpoint_port": "Manual public port", + "manual_endpoint_port_message": "Optional public port from 1 to 65535.", + "app_port": "App port", + "app_port_message": "Internal container port exposed to Coolify routing.", + "auto_gateway_endpoint": "Automatic gateway endpoint", + "pending_automatic_endpoint": "Automatic endpoint resolution pending", "auto_deploy": "Auto-Deploy", "auto_deploy_tooltip": "Automatisch Deployments erstellen, wenn sich dieses Ziel ändert.", "coolify_ssl": "Coolify SSL", diff --git a/src/i18n/generated/en-v2.json b/src/i18n/generated/en-v2.json index 677734b8..4254e454 100644 --- a/src/i18n/generated/en-v2.json +++ b/src/i18n/generated/en-v2.json @@ -2350,6 +2350,21 @@ "add_suggested_channel": "Add suggested channel", "module_health_empty": "Module health snapshots appear after probes have been recorded." }, + "status": { + "confirm_issue_action": "Run this Release Manager action? It can change deployment state and will be audited.", + "choose_bundle_prompt": "Enter the bundle id to set for this channel.", + "impact": "Impact", + "cause": "Cause", + "automated_fix": "Automated fix", + "manual_fallback": "Manual fallback", + "related_deployment": "Related deployment", + "recent_result": "Recent result", + "no_automated_action": "No automated action available.", + "deployment": "Deployment", + "target": "Target", + "coolify_target": "Coolify target", + "service_set": "Service set" + }, "settings": { "title": "Release Manager Settings", "subtitle": "Configure the GitHub token used for private repositories and branch lookups.", @@ -2450,7 +2465,8 @@ "release_runtime": "Release runtime", "bundle": "Bundle", "frontend": "Frontend", - "api": "API" + "api": "API", + "switch_error": "Release channel could not be switched. The previous channel is still active." }, "common": { "yes": "yes", @@ -2462,6 +2478,7 @@ "rollback": "Rollback", "update": "Update", "create": "Create", + "publish_release": "Publish release", "clear": "Clear", "assign": "Assign", "remove": "Remove", @@ -2714,6 +2731,19 @@ "ssl_domain_message": "Must be a DNS domain routed to the Coolify load balancer. Example: api-v2.truckwash.io", "ssl_domain_placeholder": "Load balancer domain", "https_domain_for_coolify": "Domain controlled by the load balancer", + "endpoint_mode": "Endpoint mode", + "endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.", + "endpoint_mode_auto": "Auto", + "endpoint_mode_manual": "Manual", + "manual_endpoint_host": "Manual host", + "manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.", + "manual_endpoint_host_placeholder": "api-v2.truckwash.io", + "manual_endpoint_port": "Manual public port", + "manual_endpoint_port_message": "Optional public port from 1 to 65535.", + "app_port": "App port", + "app_port_message": "Internal container port exposed to Coolify routing.", + "auto_gateway_endpoint": "Automatic gateway endpoint", + "pending_automatic_endpoint": "Automatic endpoint resolution pending", "auto_deploy": "Auto deploy", "auto_deploy_tooltip": "Automatically create deployments when this target changes.", "coolify_ssl": "Coolify SSL", diff --git a/src/i18n/generated/global-v2.json b/src/i18n/generated/global-v2.json index ea3f3c53..ad4ef88a 100644 --- a/src/i18n/generated/global-v2.json +++ b/src/i18n/generated/global-v2.json @@ -1939,6 +1939,19 @@ "ssl_domain_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain_message'}", "ssl_domain_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.ssl_domain_placeholder'}", "https_domain_for_coolify": "@:{'templates.generated.compat.configuration.release_manager.integrations.https_domain_for_coolify'}", + "endpoint_mode": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode'}", + "endpoint_mode_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_message'}", + "endpoint_mode_auto": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_auto'}", + "endpoint_mode_manual": "@:{'templates.generated.compat.configuration.release_manager.integrations.endpoint_mode_manual'}", + "manual_endpoint_host": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host'}", + "manual_endpoint_host_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host_message'}", + "manual_endpoint_host_placeholder": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_host_placeholder'}", + "manual_endpoint_port": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_port'}", + "manual_endpoint_port_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.manual_endpoint_port_message'}", + "app_port": "@:{'templates.generated.compat.configuration.release_manager.integrations.app_port'}", + "app_port_message": "@:{'templates.generated.compat.configuration.release_manager.integrations.app_port_message'}", + "auto_gateway_endpoint": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_gateway_endpoint'}", + "pending_automatic_endpoint": "@:{'templates.generated.compat.configuration.release_manager.integrations.pending_automatic_endpoint'}", "auto_deploy": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_deploy'}", "auto_deploy_tooltip": "@:{'templates.generated.compat.configuration.release_manager.integrations.auto_deploy_tooltip'}", "coolify_ssl": "@:{'templates.generated.compat.configuration.release_manager.integrations.coolify_ssl'}", diff --git a/src/i18n/generated/no-v2.json b/src/i18n/generated/no-v2.json index cd9f116e..05a993fc 100644 --- a/src/i18n/generated/no-v2.json +++ b/src/i18n/generated/no-v2.json @@ -2991,6 +2991,19 @@ "ssl_domain_message": "Må være et DNS-domene som routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io", "ssl_domain_placeholder": "Load balancer-domene", "https_domain_for_coolify": "Domene kontrollert av load balanceren", + "endpoint_mode": "Endpoint mode", + "endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.", + "endpoint_mode_auto": "Auto", + "endpoint_mode_manual": "Manual", + "manual_endpoint_host": "Manual host", + "manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.", + "manual_endpoint_host_placeholder": "api-v2.truckwash.io", + "manual_endpoint_port": "Manual public port", + "manual_endpoint_port_message": "Optional public port from 1 to 65535.", + "app_port": "App port", + "app_port_message": "Internal container port exposed to Coolify routing.", + "auto_gateway_endpoint": "Automatic gateway endpoint", + "pending_automatic_endpoint": "Automatic endpoint resolution pending", "auto_deploy": "Auto deploy", "auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.", "coolify_ssl": "Coolify SSL", diff --git a/src/i18n/generated/sv-v2.json b/src/i18n/generated/sv-v2.json index 6b182ef1..2e9d8792 100644 --- a/src/i18n/generated/sv-v2.json +++ b/src/i18n/generated/sv-v2.json @@ -3041,6 +3041,19 @@ "ssl_domain_message": "Måste vara en DNS-domän som routas till Coolify load balancern. Exempel: api-v2.truckwash.io", "ssl_domain_placeholder": "Load balancer-domän", "https_domain_for_coolify": "Domän som kontrolleras av load balancern", + "endpoint_mode": "Endpoint mode", + "endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.", + "endpoint_mode_auto": "Auto", + "endpoint_mode_manual": "Manual", + "manual_endpoint_host": "Manual host", + "manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.", + "manual_endpoint_host_placeholder": "api-v2.truckwash.io", + "manual_endpoint_port": "Manual public port", + "manual_endpoint_port_message": "Optional public port from 1 to 65535.", + "app_port": "App port", + "app_port_message": "Internal container port exposed to Coolify routing.", + "auto_gateway_endpoint": "Automatic gateway endpoint", + "pending_automatic_endpoint": "Automatic endpoint resolution pending", "auto_deploy": "Auto deploy", "auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.", "coolify_ssl": "Coolify SSL", diff --git a/src/i18n/source/da/phrases/compat/configuration/index.json b/src/i18n/source/da/phrases/compat/configuration/index.json index 8e60b4d8..45dc1230 100644 --- a/src/i18n/source/da/phrases/compat/configuration/index.json +++ b/src/i18n/source/da/phrases/compat/configuration/index.json @@ -377,6 +377,21 @@ "add_suggested_channel": "Tilføj foreslået kanal", "module_health_empty": "Modul-health snapshots vises, når probes er blevet registreret." }, + "status": { + "confirm_issue_action": "Koer denne Release Manager-handling? Den kan aendre deployment-tilstand og bliver auditeret.", + "choose_bundle_prompt": "Indtast bundle-id'et, der skal saettes for denne kanal.", + "impact": "Konsekvens", + "cause": "Aarsag", + "automated_fix": "Automatisk rettelse", + "manual_fallback": "Manuel fallback", + "related_deployment": "Relateret deployment", + "recent_result": "Seneste resultat", + "no_automated_action": "Ingen automatisk handling tilgaengelig.", + "deployment": "Deployment", + "target": "Maal", + "coolify_target": "Coolify-maal", + "service_set": "Service set" + }, "settings": { "title": "Release Manager-indstillinger", "subtitle": "Konfigurer GitHub-tokenet, der bruges til private repositories og branch-opslag.", @@ -477,7 +492,8 @@ "release_runtime": "Release-runtime", "bundle": "Bundle", "frontend": "Frontend", - "api": "API" + "api": "API", + "switch_error": "Release-kanalen kunne ikke skiftes. Den forrige kanal er stadig aktiv." }, "common": { "yes": "ja", @@ -489,6 +505,7 @@ "rollback": "Rul tilbage", "update": "Opdater", "create": "Opret", + "publish_release": "Publicer release", "clear": "Ryd", "assign": "Tildel", "remove": "Fjern", @@ -741,6 +758,19 @@ "ssl_domain_message": "Skal være et DNS-domæne, der routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io", "ssl_domain_placeholder": "Load balancer-domæne", "https_domain_for_coolify": "Domæne kontrolleret af load balanceren", + "endpoint_mode": "Endpoint mode", + "endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.", + "endpoint_mode_auto": "Auto", + "endpoint_mode_manual": "Manual", + "manual_endpoint_host": "Manual host", + "manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.", + "manual_endpoint_host_placeholder": "api-v2.truckwash.io", + "manual_endpoint_port": "Manual public port", + "manual_endpoint_port_message": "Optional public port from 1 to 65535.", + "app_port": "App port", + "app_port_message": "Internal container port exposed to Coolify routing.", + "auto_gateway_endpoint": "Automatic gateway endpoint", + "pending_automatic_endpoint": "Automatic endpoint resolution pending", "auto_deploy": "Auto deploy", "auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.", "coolify_ssl": "Coolify SSL", diff --git a/src/i18n/source/de/phrases/compat/configuration/index.json b/src/i18n/source/de/phrases/compat/configuration/index.json index 653b487d..e2e817d6 100644 --- a/src/i18n/source/de/phrases/compat/configuration/index.json +++ b/src/i18n/source/de/phrases/compat/configuration/index.json @@ -741,6 +741,19 @@ "ssl_domain_message": "Muss eine DNS-Domain sein, die zum Coolify Load Balancer geroutet wird. Beispiel: api-v2.truckwash.io", "ssl_domain_placeholder": "Load-Balancer-Domain", "https_domain_for_coolify": "Domain unter Kontrolle des Load Balancers", + "endpoint_mode": "Endpoint mode", + "endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.", + "endpoint_mode_auto": "Auto", + "endpoint_mode_manual": "Manual", + "manual_endpoint_host": "Manual host", + "manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.", + "manual_endpoint_host_placeholder": "api-v2.truckwash.io", + "manual_endpoint_port": "Manual public port", + "manual_endpoint_port_message": "Optional public port from 1 to 65535.", + "app_port": "App port", + "app_port_message": "Internal container port exposed to Coolify routing.", + "auto_gateway_endpoint": "Automatic gateway endpoint", + "pending_automatic_endpoint": "Automatic endpoint resolution pending", "auto_deploy": "Auto-Deploy", "auto_deploy_tooltip": "Automatisch Deployments erstellen, wenn sich dieses Ziel ändert.", "coolify_ssl": "Coolify SSL", diff --git a/src/i18n/source/en/phrases/compat/configuration/index.json b/src/i18n/source/en/phrases/compat/configuration/index.json index c2346ac5..e7ec8d2f 100644 --- a/src/i18n/source/en/phrases/compat/configuration/index.json +++ b/src/i18n/source/en/phrases/compat/configuration/index.json @@ -377,6 +377,21 @@ "add_suggested_channel": "Add suggested channel", "module_health_empty": "Module health snapshots appear after probes have been recorded." }, + "status": { + "confirm_issue_action": "Run this Release Manager action? It can change deployment state and will be audited.", + "choose_bundle_prompt": "Enter the bundle id to set for this channel.", + "impact": "Impact", + "cause": "Cause", + "automated_fix": "Automated fix", + "manual_fallback": "Manual fallback", + "related_deployment": "Related deployment", + "recent_result": "Recent result", + "no_automated_action": "No automated action available.", + "deployment": "Deployment", + "target": "Target", + "coolify_target": "Coolify target", + "service_set": "Service set" + }, "settings": { "title": "Release Manager Settings", "subtitle": "Configure the GitHub token used for private repositories and branch lookups.", @@ -477,7 +492,8 @@ "release_runtime": "Release runtime", "bundle": "Bundle", "frontend": "Frontend", - "api": "API" + "api": "API", + "switch_error": "Release channel could not be switched. The previous channel is still active." }, "common": { "yes": "yes", @@ -489,6 +505,7 @@ "rollback": "Rollback", "update": "Update", "create": "Create", + "publish_release": "Publish release", "clear": "Clear", "assign": "Assign", "remove": "Remove", @@ -741,6 +758,19 @@ "ssl_domain_message": "Must be a DNS domain routed to the Coolify load balancer. Example: api-v2.truckwash.io", "ssl_domain_placeholder": "Load balancer domain", "https_domain_for_coolify": "Domain controlled by the load balancer", + "endpoint_mode": "Endpoint mode", + "endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.", + "endpoint_mode_auto": "Auto", + "endpoint_mode_manual": "Manual", + "manual_endpoint_host": "Manual host", + "manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.", + "manual_endpoint_host_placeholder": "api-v2.truckwash.io", + "manual_endpoint_port": "Manual public port", + "manual_endpoint_port_message": "Optional public port from 1 to 65535.", + "app_port": "App port", + "app_port_message": "Internal container port exposed to Coolify routing.", + "auto_gateway_endpoint": "Automatic gateway endpoint", + "pending_automatic_endpoint": "Automatic endpoint resolution pending", "auto_deploy": "Auto deploy", "auto_deploy_tooltip": "Automatically create deployments when this target changes.", "coolify_ssl": "Coolify SSL", diff --git a/src/i18n/source/global/shared/configuration/index.json b/src/i18n/source/global/shared/configuration/index.json index 557df65f..38314303 100644 --- a/src/i18n/source/global/shared/configuration/index.json +++ b/src/i18n/source/global/shared/configuration/index.json @@ -794,6 +794,19 @@ "ssl_domain_message": "@:{'phrases.compat.configuration.release_manager.integrations.ssl_domain_message'}", "ssl_domain_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.ssl_domain_placeholder'}", "https_domain_for_coolify": "@:{'phrases.compat.configuration.release_manager.integrations.https_domain_for_coolify'}", + "endpoint_mode": "@:{'phrases.compat.configuration.release_manager.integrations.endpoint_mode'}", + "endpoint_mode_message": "@:{'phrases.compat.configuration.release_manager.integrations.endpoint_mode_message'}", + "endpoint_mode_auto": "@:{'phrases.compat.configuration.release_manager.integrations.endpoint_mode_auto'}", + "endpoint_mode_manual": "@:{'phrases.compat.configuration.release_manager.integrations.endpoint_mode_manual'}", + "manual_endpoint_host": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_host'}", + "manual_endpoint_host_message": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_host_message'}", + "manual_endpoint_host_placeholder": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_host_placeholder'}", + "manual_endpoint_port": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_port'}", + "manual_endpoint_port_message": "@:{'phrases.compat.configuration.release_manager.integrations.manual_endpoint_port_message'}", + "app_port": "@:{'phrases.compat.configuration.release_manager.integrations.app_port'}", + "app_port_message": "@:{'phrases.compat.configuration.release_manager.integrations.app_port_message'}", + "auto_gateway_endpoint": "@:{'phrases.compat.configuration.release_manager.integrations.auto_gateway_endpoint'}", + "pending_automatic_endpoint": "@:{'phrases.compat.configuration.release_manager.integrations.pending_automatic_endpoint'}", "auto_deploy": "@:{'phrases.compat.configuration.release_manager.integrations.auto_deploy'}", "auto_deploy_tooltip": "@:{'phrases.compat.configuration.release_manager.integrations.auto_deploy_tooltip'}", "coolify_ssl": "@:{'phrases.compat.configuration.release_manager.integrations.coolify_ssl'}", diff --git a/src/i18n/source/no/phrases/compat/configuration/index.json b/src/i18n/source/no/phrases/compat/configuration/index.json index 2fb81318..ea8d9f1e 100644 --- a/src/i18n/source/no/phrases/compat/configuration/index.json +++ b/src/i18n/source/no/phrases/compat/configuration/index.json @@ -741,6 +741,19 @@ "ssl_domain_message": "Må være et DNS-domene som routes til Coolify load balanceren. Eksempel: api-v2.truckwash.io", "ssl_domain_placeholder": "Load balancer-domene", "https_domain_for_coolify": "Domene kontrollert av load balanceren", + "endpoint_mode": "Endpoint mode", + "endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.", + "endpoint_mode_auto": "Auto", + "endpoint_mode_manual": "Manual", + "manual_endpoint_host": "Manual host", + "manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.", + "manual_endpoint_host_placeholder": "api-v2.truckwash.io", + "manual_endpoint_port": "Manual public port", + "manual_endpoint_port_message": "Optional public port from 1 to 65535.", + "app_port": "App port", + "app_port_message": "Internal container port exposed to Coolify routing.", + "auto_gateway_endpoint": "Automatic gateway endpoint", + "pending_automatic_endpoint": "Automatic endpoint resolution pending", "auto_deploy": "Auto deploy", "auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.", "coolify_ssl": "Coolify SSL", diff --git a/src/i18n/source/sv/phrases/compat/configuration/index.json b/src/i18n/source/sv/phrases/compat/configuration/index.json index 6f0b1434..9d94f597 100644 --- a/src/i18n/source/sv/phrases/compat/configuration/index.json +++ b/src/i18n/source/sv/phrases/compat/configuration/index.json @@ -741,6 +741,19 @@ "ssl_domain_message": "Måste vara en DNS-domän som routas till Coolify load balancern. Exempel: api-v2.truckwash.io", "ssl_domain_placeholder": "Load balancer-domän", "https_domain_for_coolify": "Domän som kontrolleras av load balancern", + "endpoint_mode": "Endpoint mode", + "endpoint_mode_message": "Use automatic gateway routing or manually override the public host and port.", + "endpoint_mode_auto": "Auto", + "endpoint_mode_manual": "Manual", + "manual_endpoint_host": "Manual host", + "manual_endpoint_host_message": "Public host shown in release previews when manual mode is selected.", + "manual_endpoint_host_placeholder": "api-v2.truckwash.io", + "manual_endpoint_port": "Manual public port", + "manual_endpoint_port_message": "Optional public port from 1 to 65535.", + "app_port": "App port", + "app_port_message": "Internal container port exposed to Coolify routing.", + "auto_gateway_endpoint": "Automatic gateway endpoint", + "pending_automatic_endpoint": "Automatic endpoint resolution pending", "auto_deploy": "Auto deploy", "auto_deploy_tooltip": "Opret automatisk deployments, når dette mål ændres.", "coolify_ssl": "Coolify SSL", diff --git a/src/services/releaseChannelAvailability.js b/src/services/releaseChannelAvailability.js index bc97205d..c16067d0 100644 --- a/src/services/releaseChannelAvailability.js +++ b/src/services/releaseChannelAvailability.js @@ -391,6 +391,35 @@ export const selectReleaseChannel = (channelOrSlug) => { return slug; }; +export const switchSelectedReleaseChannel = async (channelOrSlug, refreshRuntime) => { + if (typeof refreshRuntime !== "function") { + throw new Error("Release runtime refresh is not available."); + } + + const previousSlug = getSelectedReleaseChannelSlug(); + const targetSlug = selectReleaseChannel(channelOrSlug); + if (!targetSlug) { + return ""; + } + + try { + const runtime = (await refreshRuntime({ throwOnError: true })) || releaseRuntimeState; + const confirmedSlug = releaseChannelKey(runtime?.channel || releaseRuntimeState.channel); + if (confirmedSlug !== targetSlug) { + throw new Error(`Release runtime switched to ${confirmedSlug || "unknown"} instead of ${targetSlug}.`); + } + return targetSlug; + } catch (error) { + selectReleaseChannel(previousSlug); + try { + await refreshRuntime({ throwOnError: true }); + } catch (restoreError) { + console.warn("Could not restore the previous release runtime after a failed switch.", restoreError); + } + throw error; + } +}; + export const clearSelectedReleaseChannel = () => { state.selectedChannelSlug = ""; writeSelectedChannelSlug(""); diff --git a/src/services/superuserReleases.js b/src/services/superuserReleases.js index d1c314dd..2ac852c7 100644 --- a/src/services/superuserReleases.js +++ b/src/services/superuserReleases.js @@ -236,6 +236,9 @@ export const startReleaseDeployment = (payload) => export const promoteReleaseDeployment = (id) => requestReleaseManager(`/superuser/releases/deployments/${id}/promote`, "POST", {}); +export const runReleaseIssueAction = (payload) => + requestReleaseManager("/superuser/releases/issues/actions", "POST", payload); + export const setReleaseReplayTarget = (payload) => requestReleaseManager("/superuser/releases/replay-targets", "POST", payload); diff --git a/src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue b/src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue index 1c768b93..64fe6b0f 100644 --- a/src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue +++ b/src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue @@ -30,6 +30,7 @@ import { listReleaseGithubRepositories, promoteReleaseBundle, promoteReleaseDeployment, + requestReleaseManager, rollbackReleaseChannel, saveReleaseDeploymentTarget, releaseManagerControlApiCandidates, @@ -52,6 +53,8 @@ const DEFAULT_RELEASE_BRANCH = "master"; const RELEASE_STATUS_SERVICES = ["frontend", "api", "database", "redis", "minio"]; const RELEASE_DATA_SERVICES = ["database", "redis", "minio"]; const COOLIFY_PENDING_ENDPOINT = "created by Coolify; host/port assigned after deployment"; +const runReleaseIssueAction = (payload) => + requestReleaseManager("/superuser/releases/issues/actions", "POST", payload); const SERVICE_CONFIGURATION_PENDING_ENDPOINT = "host/port assigned after service is configured"; const REPLICA_PENDING_ENDPOINT = "host/port assigned after replica provisioning"; const valueLabel = (group, value) => trFallback(`values.${group}.${value}`, String(value || "")); @@ -143,6 +146,7 @@ const bundleAccessResult = ref({ frontend: null, api: null, }); +const releaseIssueActionResult = ref(null); const releaseFlowStep = ref(0); const showAdvancedDeployment = ref(false); const createdBundle = ref(null); @@ -200,6 +204,10 @@ const targetForm = reactive({ coolify_domain: "", coolify_public_url: "", coolify_deploy_now: false, + endpoint_mode: "auto", + manual_endpoint_host: "", + manual_endpoint_port: "", + coolify_ports_exposes: "80", }); const deploymentForm = reactive({ @@ -329,41 +337,31 @@ const setupSteps = computed(() => [ key: "channels", label: tr("setup.channels"), done: channels.value.length > 0, - action: () => { - activeTab.value = "channels"; - }, + action: () => scrollToSection("channels"), }, { key: "targets", label: tr("setup.targets"), done: targets.value.length > 0, - action: () => { - activeTab.value = "integrations"; - }, + action: () => scrollToSection("integrations"), }, { key: "assignments", label: tr("setup.assignments"), done: assignments.value.length > 0, - action: () => { - activeTab.value = "assignments"; - }, + action: () => scrollToSection("assignments"), }, { key: "deployments", label: tr("setup.deployments"), done: deployments.value.length > 0, - action: () => { - activeTab.value = "deployments"; - }, + action: () => scrollToSection("deployments"), }, { key: "replay", label: tr("setup.replay"), done: channels.value.some((channel) => channel.replay_enabled) || timelineSummary.value.events > 0, - action: () => { - activeTab.value = "replay"; - }, + action: () => scrollToSection("replay"), }, ]); const controlApiCandidates = computed(() => releaseManagerControlApiCandidates()); @@ -692,8 +690,20 @@ const statusDrawerService = computed(() => { ) || null ); }); -const activeTabMeta = computed(() => tabs.value.find((tab) => tab.key === activeTab.value) || tabs.value[0]); const releasePrimaryIssue = computed(() => statusIssues.value[0] || null); + +function scrollToSection(section) { + const key = normalizeReleaseTargetTab(section); + activeTab.value = key; + if (typeof document === "undefined") { + return; + } + requestAnimationFrame(() => { + const target = document.querySelector(`[data-release-section="${key}"]`); + target?.scrollIntoView({ behavior: "smooth", block: "start" }); + }); +} + const releaseCockpitSeverity = computed(() => { if ((statusTotals.value.critical || 0) > 0) { return "critical"; @@ -733,7 +743,7 @@ const releaseCockpitSubtitle = computed(() => { const releaseCockpitPrimaryAction = computed(() => releasePrimaryIssue.value ? releaseIssueActionLabel(releasePrimaryIssue.value) - : trFallback("actions.create_release", "Create release") + : trFallback("actions.publish_release", "Publish release") ); const normalizedGithubApiUrl = computed(() => String(releaseConfigForm.github_api_url || "").trim()); const isGithubApiUrlValid = computed(() => /^https?:\/\/[^\s]+$/i.test(normalizedGithubApiUrl.value)); @@ -861,7 +871,8 @@ function normalizeReleaseStatusService(service) { } function normalizeReleaseStatusIssue(issue) { - return { + const normalized = { + key: issue?.key || "", severity: issue?.severity || "warning", type: issue?.type || "stale_unknown", channel_id: issue?.channel_id ?? null, @@ -874,7 +885,26 @@ function normalizeReleaseStatusIssue(issue) { target_id: issue?.target_id ?? null, deployment_id: issue?.deployment_id ?? null, coolify_target_id: issue?.coolify_target_id ?? null, + service_set_id: issue?.service_set_id ?? null, missing_key: issue?.missing_key || null, + impact: issue?.impact || "", + resolution_state: issue?.resolution_state || "open", + actions: Array.isArray(issue?.actions) ? issue.actions.map(normalizeReleaseStatusAction) : [], + }; + normalized.key = normalized.key || releaseStatusIssueKey(normalized); + return normalized; +} + +function normalizeReleaseStatusAction(action) { + return { + id: action?.id || "", + label: action?.label || readableValue(action?.id), + kind: action?.kind || "mutation", + requires_confirmation: Boolean(action?.requires_confirmation), + requires_input: Boolean(action?.requires_input), + disabled_reason: action?.disabled_reason || "", + permission: action?.permission || "superuser_release_manager_deploy", + bundle_choices: Array.isArray(action?.bundle_choices) ? action.bundle_choices : [], }; } @@ -1121,7 +1151,7 @@ function normalizeSettingsGuideStep(step) { } function openGithubSettings(step = 0) { - activeTab.value = "settings"; + scrollToSection("settings"); settingsGuideStep.value = normalizeSettingsGuideStep(step); } @@ -1595,6 +1625,7 @@ function selectCoolifyService(service) { targetForm.coolify_public_url = publicUrl; targetForm.health_url = healthUrlForBaseUrl(targetForm.app, publicUrl); } + targetForm.endpoint_mode = "auto"; targetForm.coolify_enable_ssl = true; } @@ -1626,8 +1657,12 @@ function prepareCoolifySslTarget(app, url) { coolify_domain: coolifyDomain, coolify_public_url: baseUrl, coolify_deploy_now: true, + endpoint_mode: "auto", + manual_endpoint_host: "", + manual_endpoint_port: "", + coolify_ports_exposes: targetForm.coolify_ports_exposes || "80", }); - activeTab.value = "integrations"; + scrollToSection("integrations"); } function suggestedHealthUrl(app = targetForm.app) { @@ -1651,7 +1686,7 @@ function applyChannelPreset(preset) { capture_level: preset.capture_level || "metadata", retention_days: Number(preset.retention_days || 14), }); - activeTab.value = "channels"; + scrollToSection("channels"); } function applyTargetPreset(preset) { @@ -1680,8 +1715,12 @@ function applyTargetPreset(preset) { coolify_domain: loadBalancerDomainValue(preset.coolify_domain), coolify_public_url: preset.coolify_public_url || preset.public_url || "", coolify_deploy_now: Boolean(preset.coolify_deploy_now), + endpoint_mode: preset.endpoint_mode === "manual" ? "manual" : "auto", + manual_endpoint_host: preset.manual_endpoint_host || "", + manual_endpoint_port: preset.manual_endpoint_port || "", + coolify_ports_exposes: String(preset.coolify_ports_exposes || preset.ports_exposes || targetForm.coolify_ports_exposes || "80"), }); - activeTab.value = "integrations"; + scrollToSection("integrations"); } function applyUrlSuggestion(kind, value) { @@ -1708,7 +1747,7 @@ function fillDeploymentFromTarget(target = selectedTarget.value) { function applyQuickDeployTarget(target) { deploymentForm.target_id = target.id; fillDeploymentFromTarget(target); - activeTab.value = "deployments"; + scrollToSection("deployments"); } function deploymentVersionLabelFromTarget(target) { @@ -2105,8 +2144,31 @@ function targetContext(target) { return fullDeploymentTarget(target)?.deploy_context || {}; } +function endpointDisplayValue(endpoint) { + if (!endpoint || typeof endpoint !== "object") { + return ""; + } + const urlEndpoint = endpointFromUrl(endpoint.url); + if (urlEndpoint) { + return urlEndpoint; + } + return endpointFromHostPort(endpoint.host, endpoint.port) || String(endpoint.message || "").trim(); +} + +function targetEndpointPending(target) { + const endpoint = fullDeploymentTarget(target)?.endpoint; + if (!endpoint || typeof endpoint !== "object") { + return false; + } + return endpoint.status !== "resolved"; +} + function targetPublicEndpoint(target) { const fullTarget = fullDeploymentTarget(target); + const resolvedEndpoint = endpointDisplayValue(fullTarget?.endpoint); + if (resolvedEndpoint) { + return resolvedEndpoint; + } const context = targetContext(fullTarget); const directUrl = firstText(context.coolify_public_url, fullTarget?.health_url, context.health_url); const directEndpoint = endpointFromUrl(directUrl); @@ -2168,7 +2230,7 @@ function dataServiceName(service, fallback) { } function dataServiceEndpoint(service) { - return endpointFromHostPort(service?.replication?.host, service?.replication?.port); + return endpointDisplayValue(service?.endpoint) || endpointFromHostPort(service?.replication?.host, service?.replication?.port); } function dataServiceLocation(service) { @@ -2181,6 +2243,10 @@ function dataServiceLocation(service) { ]); } +function automaticGatewayEndpoint() { + return endpointFromHostPort(firstAvailable(loadBalancerDomainSuggestions.value, "api-v2.truckwash.io"), "443"); +} + function isolatedPlacementLocation(app) { const sourceTarget = isolatedStackSourceTarget(app === "api" ? "api" : "frontend"); const instanceId = Number(sourceTarget?.coolify_instance_id || coolifyInstances.value[0]?.id || 0); @@ -2215,7 +2281,7 @@ function previewAppRow(app) { type: "is-success is-light", source: targetName(sourceTarget, trFallback("bundles.preview.source_defaults", "source target defaults")), target: isolatedStackServiceName(app), - endpoint: COOLIFY_PENDING_ENDPOINT, + endpoint: automaticGatewayEndpoint() || trFallback("integrations.pending_automatic_endpoint", "automatic endpoint resolution pending"), endpointPending: true, location: isolatedPlacementLocation(app), dataPolicy: trFallback("bundles.preview.code_target_only", "code target only"), @@ -2230,8 +2296,8 @@ function previewAppRow(app) { type: "is-info is-light", source: targetName(sourceTarget, trFallback("bundles.preview.no_source_target", "no source target")), target: targetName(target, trFallback("bundles.preview.target_needed", "target needed")), - endpoint: endpoint || trFallback("bundles.preview.endpoint_not_configured", "endpoint not configured"), - endpointPending: false, + endpoint: endpoint || trFallback("integrations.pending_automatic_endpoint", "automatic endpoint resolution pending"), + endpointPending: targetEndpointPending(target) || !endpoint, location: targetLocation(target), dataPolicy: trFallback("bundles.preview.code_target_only", "code target only"), }; @@ -2592,6 +2658,13 @@ async function createIsolatedStackDeploymentTarget(app) { coolify_domain: "", coolify_public_url: "", coolify_deploy_now: true, + endpoint_mode: "auto", + manual_endpoint_host: "", + manual_endpoint_port: "", + coolify_ports_exposes: + sourceTarget?.deploy_context?.coolify_ports_exposes || + sourceTarget?.deploy_context?.ports_exposes || + (app === "api" ? "80" : "80"), coolify_project_uuid: isolatedStackProjectUuid(instanceId, sourceTarget), coolify_environment_uuid: "", coolify_environment_name: releaseCoolifyEnvironmentName(selectedBundleChannel()?.slug || "", branch), @@ -2800,7 +2873,7 @@ function editChannel(channel) { capture_level: channel.capture_level || "metadata", retention_days: Number(channel.retention_days || 14), }); - activeTab.value = "channels"; + scrollToSection("channels"); } function resetChannelForm() { @@ -2857,15 +2930,35 @@ async function saveTarget() { const coolifyDomain = domainFromValue(targetForm.coolify_domain); const coolifyPublicUrl = publicBaseUrlFromValue(targetForm.coolify_public_url) || (coolifyDomain ? `https://${coolifyDomain}` : ""); + const endpointMode = targetForm.endpoint_mode === "manual" ? "manual" : "auto"; + const manualEndpointHost = String(targetForm.manual_endpoint_host || "").trim(); + const manualEndpointPort = String(targetForm.manual_endpoint_port || "").trim(); + const coolifyPortsExposes = String(targetForm.coolify_ports_exposes || "80").trim() || "80"; + if (endpointMode === "manual" && !manualEndpointHost) { + throw new Error(trFallback("errors.manual_endpoint_host_required", "Manual endpoint mode requires a public host.")); + } + if ( + manualEndpointPort && + (!/^\d+$/.test(manualEndpointPort) || Number(manualEndpointPort) < 1 || Number(manualEndpointPort) > 65535) + ) { + throw new Error(trFallback("errors.manual_endpoint_port_invalid", "Manual endpoint port must be between 1 and 65535.")); + } const deployContext = { - ...defaultCoolifyApplicationContext(targetForm.app, { coolify_build_pack: targetForm.coolify_build_pack }), + ...defaultCoolifyApplicationContext(targetForm.app, { + coolify_build_pack: targetForm.coolify_build_pack, + coolify_ports_exposes: coolifyPortsExposes, + }), coolify_auto_create: Boolean(targetForm.coolify_auto_create), - coolify_enable_ssl: Boolean(targetForm.coolify_enable_ssl && (coolifyPublicUrl || coolifyDomain)), + coolify_enable_ssl: Boolean(targetForm.coolify_enable_ssl), coolify_domain: coolifyDomain, coolify_public_url: coolifyPublicUrl, coolify_deploy_now: Boolean(targetForm.coolify_deploy_now), coolify_project_uuid: targetForm.coolify_project_uuid || "", coolify_github_app_uuid: String(targetForm.coolify_github_app_uuid || "").trim(), + endpoint_mode: endpointMode, + manual_endpoint_host: endpointMode === "manual" ? manualEndpointHost : "", + manual_endpoint_port: endpointMode === "manual" ? manualEndpointPort : "", + coolify_ports_exposes: coolifyPortsExposes, }; const savedResponse = await saveReleaseDeploymentTarget({ ...targetForm, @@ -2893,6 +2986,10 @@ async function saveTarget() { coolify_domain: "", coolify_public_url: "", coolify_deploy_now: false, + endpoint_mode: "auto", + manual_endpoint_host: "", + manual_endpoint_port: "", + coolify_ports_exposes: "80", }); targetAccessResult.value = null; await load(); @@ -2919,7 +3016,7 @@ async function removeTarget(target) { function useTargetForDeployment(target) { deploymentForm.target_id = target.id; fillDeploymentFromTarget(target); - activeTab.value = "deployments"; + scrollToSection("deployments"); } async function redeployTarget(target) { @@ -3091,6 +3188,9 @@ function releaseStatusMaxSeverity(severities) { } function releaseStatusIssueKey(issue) { + if (issue?.key) { + return String(issue.key); + } return [ issue?.type || "", issue?.channel_id || issue?.channel_slug || "", @@ -3229,6 +3329,23 @@ function releaseStatusIssueTypeLabel(type) { ); } +function releaseStatusIssueRelationLabel(issue) { + const parts = []; + if (issue?.deployment_id) { + parts.push(`${trFallback("status.deployment", "Deployment")} #${issue.deployment_id}`); + } + if (issue?.target_id) { + parts.push(`${trFallback("status.target", "Target")} #${issue.target_id}`); + } + if (issue?.coolify_target_id) { + parts.push(`${trFallback("status.coolify_target", "Coolify target")} #${issue.coolify_target_id}`); + } + if (issue?.service_set_id) { + parts.push(`${trFallback("status.service_set", "Service set")} #${issue.service_set_id}`); + } + return parts.join(" / "); +} + function releaseStatusServiceStatusLabel(service) { const status = service?.state && service.state !== "ready" ? service.state : service?.status || "ready"; return statusLabel(status); @@ -3306,6 +3423,7 @@ function openReleaseStatusDetails(channel, issue = null, serviceKey = "") { selectedStatusChannelId.value = channel?.channel_id || channel?.id || issue?.channel_id || null; selectedStatusIssue.value = issue; selectedStatusServiceKey.value = serviceKey || issue?.service_key || ""; + releaseIssueActionResult.value = null; } function openReleaseStatusIssue(issue) { @@ -3325,15 +3443,16 @@ function closeReleaseStatusDetails() { selectedStatusChannelId.value = null; selectedStatusIssue.value = null; selectedStatusServiceKey.value = ""; + releaseIssueActionResult.value = null; } function goToReleaseStatusTab(tab) { - activeTab.value = normalizeReleaseTargetTab(tab); + scrollToSection(tab); closeReleaseStatusDetails(); } function focusReleaseCreation() { - activeTab.value = "deployments"; + scrollToSection("deployments"); closeReleaseStatusDetails(); } @@ -3343,10 +3462,58 @@ function handleReleaseIssueAction(issue = releasePrimaryIssue.value) { return; } - activeTab.value = normalizeReleaseTargetTab(issue.target_tab); + scrollToSection(issue.target_tab); openReleaseStatusIssue(issue); } +async function executeReleaseIssueAction(issue, action) { + if (!issue || !action || action.disabled_reason) { + return; + } + + const inputs = {}; + if (action.id === "set_bundle" && action.bundle_choices.length > 1) { + const selected = window.prompt( + trFallback("status.choose_bundle_prompt", "Enter the bundle id to set for this channel."), + String(action.bundle_choices[0]?.id || "") + ); + if (!selected) { + return; + } + inputs.bundle_id = selected; + } + + const confirmed = + !action.requires_confirmation || + window.confirm( + trFallback( + "status.confirm_issue_action", + "Run this Release Manager action? It can change deployment state and will be audited." + ) + ); + if (!confirmed) { + return; + } + + await run(`issue:${releaseStatusIssueKey(issue)}:${action.id}`, async () => { + const result = responseData( + await runReleaseIssueAction({ + issue_key: releaseStatusIssueKey(issue), + action_id: action.id, + inputs, + confirm: confirmed, + }), + {} + ); + releaseIssueActionResult.value = result; + if (result.summary) { + summary.value = result.summary; + } else { + await load(); + } + }); +} + function normalizedBundleStatus(bundle) { return String(bundle?.status || "").toLowerCase(); } @@ -3565,7 +3732,7 @@ onMounted(async () => { data-testid="release-cockpit-create-release" @click="focusReleaseCreation" > - {{ trFallback("actions.create_release", "Create release") }} + {{ trFallback("actions.publish_release", "Publish release") }} { {{ trFallback("status.issue_panel_hint", "Highest severity first.") }} - + +
+ + {{ action.label }} + +
+
{{ trFallback("status.no_issues", "No release blockers") }} @@ -3764,31 +3946,25 @@ onMounted(async () => { - - + + { }} - + +
+ + {{ action.label }} + +
+
{{ trFallback("status.no_issues", "No release blockers") }} @@ -4217,8 +4408,9 @@ onMounted(async () => { { { { { { + + + + + + + + + + + + + + + { { > {{ tr("settings.save") }} - + {{ tr("settings.back_to_integrations") }}
@@ -5983,6 +6217,24 @@ onMounted(async () => {
+ + {{ trFallback("status.recent_result", "Recent result") }}: + {{ releaseIssueActionResult.message }} + +
{{ trFallback("status.issues", "Issues") }} @@ -5998,18 +6250,59 @@ onMounted(async () => { >
{{ issue.label || releaseStatusIssueTypeLabel(issue.type) }} - {{ issue.message }} - {{ issue.next_action }} + {{ releaseStatusReadinessLabel(issue.resolution_state) }} +
+
+
{{ trFallback("status.cause", "Cause") }}
+
{{ issue.message || releaseStatusIssueTypeLabel(issue.type) }}
+
+
+
{{ trFallback("status.impact", "Impact") }}
+
{{ issue.impact }}
+
+
+
{{ trFallback("status.automated_fix", "Automated fix") }}
+
+ + {{ trFallback("status.no_automated_action", "No automated action available.") }} + + {{ issue.actions.map((action) => action.label).join(", ") }} +
+
+
+
{{ trFallback("status.manual_fallback", "Manual fallback") }}
+
{{ issue.next_action }}
+
+
+
{{ trFallback("status.related_deployment", "Related deployment") }}
+
{{ releaseStatusIssueRelationLabel(issue) }}
+
+
+
+
+ + {{ action.label }} + + + {{ tabs.find((tab) => tab.key === issue.target_tab)?.label || issue.target_tab }} +
- - {{ tabs.find((tab) => tab.key === issue.target_tab)?.label || issue.target_tab }} -
@@ -6154,12 +6447,34 @@ onMounted(async () => {