From 74e994b54d024434e52a9089b2174a0804e57c66 Mon Sep 17 00:00:00 2001 From: Martin Helmich Date: Fri, 3 Jul 2026 10:26:19 +0200 Subject: [PATCH 1/2] fix(cronjob): render container cron jobs in "cronjob get" A cron job may target an app installation OR a container (a service running in a stack). "cronjob get" previously always resolved the cron job's app installation, which fails or renders misleading output for container cron jobs. Branch on the cron job's target: for service (container) targets, show the stack and container instead of the app, and render the container command as the execution target. App cron jobs are rendered as before. Co-Authored-By: Claude Opus 4.8 --- .../components/CronJob/CronJobDetails.tsx | 60 +++++++++++++++++-- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/src/rendering/react/components/CronJob/CronJobDetails.tsx b/src/rendering/react/components/CronJob/CronJobDetails.tsx index f6cc2a7f9..45cddbfad 100644 --- a/src/rendering/react/components/CronJob/CronJobDetails.tsx +++ b/src/rendering/react/components/CronJob/CronJobDetails.tsx @@ -1,4 +1,4 @@ -import { FC } from "react"; +import { FC, ReactNode } from "react"; import type { MittwaldAPIV2 } from "@mittwald/api-client"; import { SingleResult } from "../SingleResult.js"; import { Value } from "../Value.js"; @@ -12,9 +12,21 @@ type CronjobCronjob = MittwaldAPIV2.Components.Schemas.CronjobCronjob; type CronjobCronjobUrl = MittwaldAPIV2.Components.Schemas.CronjobCronjobUrl; type CronjobCronjobCommand = MittwaldAPIV2.Components.Schemas.CronjobCronjobCommand; +type CronjobServiceTargetResponse = + MittwaldAPIV2.Components.Schemas.CronjobServiceTargetResponse; type CronJobComponent = FC<{ cronjob: CronjobCronjob }>; +// A cron job either targets an app installation or a container (a service +// running in a stack). Container cron jobs carry a service target instead of +// an app id, so we must not try to resolve them as app installations. +const getServiceTarget = ( + cronjob: CronjobCronjob, +): CronjobServiceTargetResponse | undefined => { + const { target } = cronjob; + return target && "stackId" in target ? target : undefined; +}; + const CronJobNextExecution: CronJobComponent = ({ cronjob }) => { if (!cronjob.nextExecutionTime) { return ; @@ -27,6 +39,26 @@ const CronJobNextExecution: CronJobComponent = ({ cronjob }) => { ); }; +const CronJobAppTarget: FC<{ appInstallationId: string }> = ({ + appInstallationId, +}) => { + const app = useAppInstallation(appInstallationId); + return ; +}; + +const CronJobExecutionTargetContainer: FC<{ + target: CronjobServiceTargetResponse; +}> = ({ target }) => { + return ( + {target.command}, + }} + /> + ); +}; + const CronJobExecutionTargetURL: FC<{ dest: CronjobCronjobUrl }> = ({ dest, }) => { @@ -61,13 +93,24 @@ const CronJobExecutionTargetCommand: FC<{ command: CronjobCronjobCommand }> = ({ export const CronJobDetails: CronJobComponent = ({ cronjob }) => { const project = cronjob.projectId ? useProject(cronjob.projectId) : null; - const app = useAppInstallation(cronjob.appId); + const serviceTarget = getServiceTarget(cronjob); - const rows = { + const rows: Record = { "Cron Job ID": , "Created At": , Project: project ? : , - App: , + ...(serviceTarget + ? { + Stack: {serviceTarget.stackId}, + Container: {serviceTarget.serviceShortId}, + } + : { + App: ( + + ), + }), Schedule: ( {cronjob.interval} (next execution:{" "} @@ -89,7 +132,14 @@ export const CronJobDetails: CronJobComponent = ({ cronjob }) => { />, ]; - if (cronjob.destination && "url" in cronjob.destination) { + if (serviceTarget) { + sections.push( + , + ); + } else if (cronjob.destination && "url" in cronjob.destination) { sections.push( Date: Fri, 3 Jul 2026 10:31:39 +0200 Subject: [PATCH 2/2] refactor(cronjob): move target into the EXECUTION TARGET section Render the app / stack / container as part of the EXECUTION TARGET section instead of the primary cron job details, keeping all "where and how is this executed" information in one place. Co-Authored-By: Claude Opus 4.8 --- .../components/CronJob/CronJobDetails.tsx | 140 +++++++----------- 1 file changed, 50 insertions(+), 90 deletions(-) diff --git a/src/rendering/react/components/CronJob/CronJobDetails.tsx b/src/rendering/react/components/CronJob/CronJobDetails.tsx index 45cddbfad..0a6b30517 100644 --- a/src/rendering/react/components/CronJob/CronJobDetails.tsx +++ b/src/rendering/react/components/CronJob/CronJobDetails.tsx @@ -9,9 +9,6 @@ import { useProject } from "../../../../lib/resources/project/hooks.js"; import { useAppInstallation } from "../../../../lib/resources/app/hooks.js"; import { FormattedDate } from "../FormattedDate.js"; type CronjobCronjob = MittwaldAPIV2.Components.Schemas.CronjobCronjob; -type CronjobCronjobUrl = MittwaldAPIV2.Components.Schemas.CronjobCronjobUrl; -type CronjobCronjobCommand = - MittwaldAPIV2.Components.Schemas.CronjobCronjobCommand; type CronjobServiceTargetResponse = MittwaldAPIV2.Components.Schemas.CronjobServiceTargetResponse; @@ -46,49 +43,48 @@ const CronJobAppTarget: FC<{ appInstallationId: string }> = ({ return ; }; -const CronJobExecutionTargetContainer: FC<{ - target: CronjobServiceTargetResponse; -}> = ({ target }) => { - return ( - {target.command}, - }} - /> - ); -}; +// Rows describing where and how the cron job is executed. For container cron +// jobs this is the stack/container running the command; for app cron jobs it +// is the app installation together with the invoked URL or command. +const buildExecutionTargetRows = ( + cronjob: CronjobCronjob, + serviceTarget: CronjobServiceTargetResponse | undefined, +): Record => { + if (serviceTarget) { + return { + Stack: {serviceTarget.stackId}, + Container: {serviceTarget.serviceShortId}, + Command: {serviceTarget.command}, + }; + } -const CronJobExecutionTargetURL: FC<{ dest: CronjobCronjobUrl }> = ({ - dest, -}) => { - return ( - {dest.url}, - }} + const app = ( + ); -}; -const CronJobExecutionTargetCommand: FC<{ command: CronjobCronjobCommand }> = ({ - command, -}) => { - return ( - {command.interpreter}, - Script: {command.path}, - Parameters: command.parameters ? ( - {command.parameters} - ) : ( - - ), - }} - /> - ); + const { destination } = cronjob; + if (destination && "url" in destination) { + return { + App: app, + URL: {destination.url}, + }; + } + if (destination) { + return { + App: app, + Interpreter: {destination.interpreter}, + Script: {destination.path}, + Parameters: destination.parameters ? ( + {destination.parameters} + ) : ( + + ), + }; + } + + return { App: app }; }; export const CronJobDetails: CronJobComponent = ({ cronjob }) => { @@ -99,18 +95,6 @@ export const CronJobDetails: CronJobComponent = ({ cronjob }) => { "Cron Job ID": , "Created At": , Project: project ? : , - ...(serviceTarget - ? { - Stack: {serviceTarget.stackId}, - Container: {serviceTarget.serviceShortId}, - } - : { - App: ( - - ), - }), Schedule: ( {cronjob.interval} (next execution:{" "} @@ -120,44 +104,20 @@ export const CronJobDetails: CronJobComponent = ({ cronjob }) => { Timezone: {cronjob.timeZone || "UTC"}, }; - const sections = [ - - CRON JOB DETAILS: {cronjob.description} - - } - rows={rows} - />, - ]; - - if (serviceTarget) { - sections.push( - , - ); - } else if (cronjob.destination && "url" in cronjob.destination) { - sections.push( - , - ); - } else if (cronjob.destination) { - sections.push( - , - ); - } - return ( - {sections} + + CRON JOB DETAILS: {cronjob.description} + + } + rows={rows} + /> + ); };