From 3836bc992df210c76fb7cd06a05f7b7dee582736 Mon Sep 17 00:00:00 2001 From: KArtHiK Date: Thu, 12 Feb 2026 14:30:11 +0100 Subject: [PATCH 1/2] Feat: Add application status --- app/pipeline/agent/agent.go | 7 + app/pipeline/agent/monitor.go | 36 +++ app/pipeline/convert/deploy_step.go | 21 +- .../convert/templates/static/3-app.yaml | 6 +- app/pipeline/convert/templates/templates.go | 35 ++- app/pipeline/manager/client/client.go | 3 + .../manager/client/embedded_client.go | 4 + app/pipeline/manager/down.go | 2 +- app/pipeline/manager/manager.go | 17 + app/pipeline/manager/up.go | 2 +- app/services/manager/kube/helper.go | 13 +- app/services/manager/kube/status.go | 290 ++++++++++++++++++ app/services/manager/manager.go | 3 + app/store/database.go | 2 +- app/store/database/application.go | 6 +- app/web/handler/project/events.go | 14 +- .../components/vapplication/app_status.templ | 28 ++ .../{status.templ => deployment_status.templ} | 17 +- .../components/vapplication/header.templ | 5 +- .../views/components/vapplication/list.templ | 2 +- types/app_status.go | 32 ++ types/enum/sse.go | 5 +- 22 files changed, 500 insertions(+), 50 deletions(-) create mode 100644 app/pipeline/agent/monitor.go create mode 100644 app/services/manager/kube/status.go create mode 100644 app/web/views/components/vapplication/app_status.templ rename app/web/views/components/vapplication/{status.templ => deployment_status.templ} (75%) create mode 100644 types/app_status.go diff --git a/app/pipeline/agent/agent.go b/app/pipeline/agent/agent.go index 801646a..80d704e 100644 --- a/app/pipeline/agent/agent.go +++ b/app/pipeline/agent/agent.go @@ -94,6 +94,13 @@ func (a *Agent) Start(ctx context.Context) error { return } }() + + go func() { + if err := a.runStatusMonitor(rCtx, *config); err != nil { + log.Ctx(ctx).Error().Err(err).Msg("agent: error running metrics scrapper") + return + } + }() // } } } diff --git a/app/pipeline/agent/monitor.go b/app/pipeline/agent/monitor.go new file mode 100644 index 0000000..1513e72 --- /dev/null +++ b/app/pipeline/agent/monitor.go @@ -0,0 +1,36 @@ +package agent + +import ( + "context" + "time" + + "github.com/cloudness-io/cloudness/types" + + "github.com/rs/zerolog/log" +) + +func (a *Agent) runStatusMonitor(ctx context.Context, config types.RunnerConfig) error { + ticker := time.NewTicker(time.Duration(5) * time.Second) + for { + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + status, err := a.serverManager.ListApplicationStatuses(ctx, nil) + if err != nil { + continue + } + + if len(status) == 0 { + log.Debug().Msg("agent: no application status found") + continue + } + + err = a.client.UploadAppStatus(ctx, status) + if err != nil { + log.Ctx(ctx).Error().Err(err).Msg("agent: error updating application status in monitor") + continue + } + } + } +} diff --git a/app/pipeline/convert/deploy_step.go b/app/pipeline/convert/deploy_step.go index 88e4b22..41b2d2f 100644 --- a/app/pipeline/convert/deploy_step.go +++ b/app/pipeline/convert/deploy_step.go @@ -87,16 +87,17 @@ func deployCommand( func getTemplateInput(image string, input *pipeline.RunnerContextInput, spec *types.ApplicationSpec, vars map[string]string) (*templates.TemplateIn, error) { in := &templates.TemplateIn{ - Namespace: input.Application.ParentSlug, - Identifier: input.Application.GetIdentifierStr(), - CloudnessIdentifier: fmt.Sprintf("%d", input.Application.UID), - Image: image, - MaxReplicas: spec.Deploy.MaxReplicas, - CPU: spec.Deploy.CPU, - Memory: spec.Deploy.Memory, - Volumes: make([]*templates.Volume, 0), - Variables: make(map[string]string), - Secrets: make(map[string]string), + Namespace: input.Application.ParentSlug, + Identifier: input.Application.GetIdentifierStr(), + CloudnessAppIdentifier: input.Application.UID, + CloudnessProjectID: input.Application.ProjectID, + Image: image, + MaxReplicas: spec.Deploy.MaxReplicas, + CPU: spec.Deploy.CPU, + Memory: spec.Deploy.Memory, + Volumes: make([]*templates.Volume, 0), + Variables: make(map[string]string), + Secrets: make(map[string]string), //networking ServicePorts: make([]int, 0), HasState: input.Application.Type == enum.ApplicationTypeStateful, diff --git a/app/pipeline/convert/templates/static/3-app.yaml b/app/pipeline/convert/templates/static/3-app.yaml index d03e717..ceedc5b 100644 --- a/app/pipeline/convert/templates/static/3-app.yaml +++ b/app/pipeline/convert/templates/static/3-app.yaml @@ -22,7 +22,8 @@ spec: labels: app.kubernetes.io/name: {{ .Identifier }} app.kubernetes.io/instance: {{ .Identifier }} - app.kubernetes.io/cloudness-identifier: "{{ .CloudnessIdentifier }}" + app.kubernetes.io/cloudness-app: "{{ .CloudnessAppIdentifier }}" + app.kubernetes.io/cloudness-project-identifier: "{{ .CloudnessProjectID }}" app.kubernetes.io/component: app app.kubernetes.io/managed-by: cloudness spec: @@ -148,7 +149,8 @@ spec: labels: app.kubernetes.io/name: {{ .Identifier }} app.kubernetes.io/instance: {{ .Identifier }} - app.kubernetes.io/cloudness-identifier: "{{ .CloudnessIdentifier }}" + app.kubernetes.io/cloudness-app: "{{ .CloudnessAppIdentifier }}" + app.kubernetes.io/cloudness-project-identifier: "{{ .CloudnessProjectID }}" app.kubernetes.io/component: app app.kubernetes.io/managed-by: cloudness spec: diff --git a/app/pipeline/convert/templates/templates.go b/app/pipeline/convert/templates/templates.go index d1cd4db..0f8bedd 100644 --- a/app/pipeline/convert/templates/templates.go +++ b/app/pipeline/convert/templates/templates.go @@ -56,23 +56,24 @@ func getTemplate(fileName string) *template.Template { type ( TemplateIn struct { - Identifier string - CloudnessIdentifier string - Namespace string - HasState bool - Image string - Command []string - Args []string - ServicePorts []int - PrivateDomain string - ServiceDomain *ServiceDomain - Volumes []*Volume - MaxReplicas int64 - CPU int64 - Memory float64 - Variables map[string]string - Secrets map[string]string - UpdatedAt string + Identifier string + CloudnessAppIdentifier int64 + CloudnessProjectID int64 + Namespace string + HasState bool + Image string + Command []string + Args []string + ServicePorts []int + PrivateDomain string + ServiceDomain *ServiceDomain + Volumes []*Volume + MaxReplicas int64 + CPU int64 + Memory float64 + Variables map[string]string + Secrets map[string]string + UpdatedAt string } ServiceDomain struct { diff --git a/app/pipeline/manager/client/client.go b/app/pipeline/manager/client/client.go index 9d0ce1e..d619579 100644 --- a/app/pipeline/manager/client/client.go +++ b/app/pipeline/manager/client/client.go @@ -38,4 +38,7 @@ type RunnerClient interface { //Metrics // UploadMetrics uploads the full metrics to server UploadMetrics(ctx context.Context, metrics []*types.AppMetrics) error + + // UploadAppStatus Uploads the application status + UploadAppStatus(ctx context.Context, status []*types.AppStatus) error } diff --git a/app/pipeline/manager/client/embedded_client.go b/app/pipeline/manager/client/embedded_client.go index 9d1af9f..e707d9d 100644 --- a/app/pipeline/manager/client/embedded_client.go +++ b/app/pipeline/manager/client/embedded_client.go @@ -80,3 +80,7 @@ func (e *embeddedClient) Upload(ctx context.Context, deploymentID int64, logs [] func (e *embeddedClient) UploadMetrics(ctx context.Context, metrics []*types.AppMetrics) error { return e.manager.UploadMetrics(ctx, metrics) } + +func (e *embeddedClient) UploadAppStatus(ctx context.Context, status []*types.AppStatus) error { + return e.manager.UploadAppStatus(ctx, status) +} diff --git a/app/pipeline/manager/down.go b/app/pipeline/manager/down.go index 3d787da..36e9c8e 100644 --- a/app/pipeline/manager/down.go +++ b/app/pipeline/manager/down.go @@ -39,7 +39,7 @@ func (m *runnerManager) down(ctx context.Context, deployment *types.Deployment) log.Warn().Err(err).Msg("manager: could not publish deployment updated event") } - if err := m.sseStreamer.Publish(ctx, app.ProjectID, enum.SSETypeApplicationUpdated, app); err != nil { + if err := m.sseStreamer.Publish(ctx, app.ProjectID, enum.SSETypeApplicationDeploymentUpdated, app); err != nil { log.Warn().Err(err).Msg("manager: could not publish application updated event") } diff --git a/app/pipeline/manager/manager.go b/app/pipeline/manager/manager.go index e5490cd..eb59bce 100644 --- a/app/pipeline/manager/manager.go +++ b/app/pipeline/manager/manager.go @@ -57,6 +57,9 @@ type ( //Metrics // UploadMetrics uploads the full metrics to server UploadMetrics(ctx context.Context, metrics []*types.AppMetrics) error + + // UploadAppStatus Uploads the application status + UploadAppStatus(ctx context.Context, status []*types.AppStatus) error } ) @@ -347,3 +350,17 @@ func (m *runnerManager) UploadMetrics(ctx context.Context, metrics []*types.AppM } return nil } + +func (m *runnerManager) UploadAppStatus(ctx context.Context, statuses []*types.AppStatus) error { + for _, status := range statuses { + if _, err := m.applicationStore.UpdateStatus(ctx, status.ApplicationUID, status.Status); err != nil { + log.Ctx(ctx).Error().Err(err).Msg("manager: error updating application status") + continue + } + + if err := m.sseStreamer.Publish(ctx, status.ProjectID, enum.SSETypeApplicationStatusUpdated, status.ToEvent()); err != nil { + log.Warn().Err(err).Msg("manager: could not publish application updated event") + } + } + return nil +} diff --git a/app/pipeline/manager/up.go b/app/pipeline/manager/up.go index e073516..ec38680 100644 --- a/app/pipeline/manager/up.go +++ b/app/pipeline/manager/up.go @@ -39,7 +39,7 @@ func (m *runnerManager) up(ctx context.Context, deployment *types.Deployment) er log.Warn().Err(err).Msg("manager: could not publish deployment updated event") } - if err := m.sseStreamer.Publish(ctx, app.ProjectID, enum.SSETypeApplicationUpdated, app); err != nil { + if err := m.sseStreamer.Publish(ctx, app.ProjectID, enum.SSETypeApplicationDeploymentUpdated, app); err != nil { log.Warn().Err(err).Msg("manager: could not publish application updated event") } diff --git a/app/services/manager/kube/helper.go b/app/services/manager/kube/helper.go index 3cd9920..4a2c5d8 100644 --- a/app/services/manager/kube/helper.go +++ b/app/services/manager/kube/helper.go @@ -42,7 +42,7 @@ func (m *K8sManager) getKubeKeyForDomain(subdomain, hostname string) (string, er } func (m *K8sManager) getApplicationUIDFromPodLabels(labels map[string]string) int64 { - appIdentifier := labels["app.kubernetes.io/cloudness-identifier"] //will be of type app-123123123 + appIdentifier := labels["app.kubernetes.io/cloudness-app"] //will be of type 123123123 if appIdentifier != "" { appUID, _ := strconv.ParseInt(appIdentifier, 10, 64) @@ -52,6 +52,17 @@ func (m *K8sManager) getApplicationUIDFromPodLabels(labels map[string]string) in return 0 } +func (m *K8sManager) getProjectIDFromPodLabels(labels map[string]string) int64 { + projectIdentifier := labels["app.kubernetes.io/cloudness-project-identifier"] //will be of type 123123123 + + if projectIdentifier != "" { + projectID, _ := strconv.ParseInt(projectIdentifier, 10, 64) + return projectID + } + + return 0 +} + func (m *K8sManager) getUpdateTimeFromPodAnnotations(labels map[string]string) int64 { updateTimeStr, found := labels["cloudness.io/deployment-time"] if !found { diff --git a/app/services/manager/kube/status.go b/app/services/manager/kube/status.go new file mode 100644 index 0000000..aaf889b --- /dev/null +++ b/app/services/manager/kube/status.go @@ -0,0 +1,290 @@ +package kube + +import ( + "context" + "fmt" + "time" + + "github.com/cloudness-io/cloudness/types" + "github.com/cloudness-io/cloudness/types/enum" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (m *K8sManager) ListApplicationStatuses(ctx context.Context, server *types.Server) ([]*types.AppStatus, error) { + client, err := m.getInterface(ctx, server) + if err != nil { + return nil, err + } + + now := time.Now().UTC() + nowMilli := now.UnixMilli() + statusByApp := make(map[int64]*types.AppStatus) + + deployments, err := client.AppsV1().Deployments(metav1.NamespaceAll).List(ctx, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/managed-by=cloudness", + }) + if err != nil { + return nil, err + } + + for _, deploy := range deployments.Items { + appUID := m.getApplicationUIDFromPodLabels(deploy.Labels) + projectID := m.getProjectIDFromPodLabels(deploy.Labels) + updatedAt := m.getUpdateTimeFromPodAnnotations(deploy.Annotations) + + if appUID == 0 { + continue + } + if updatedAt != 0 && updatedAt > (nowMilli-10_000) { + continue + } + + status, reason := evaluateDeploymentStatus(deploy) + candidate := &types.AppStatus{ + Timestamp: now, + ApplicationUID: appUID, + ProjectID: projectID, + InstanceName: getApplicationNameFromLabels(deploy.Labels, deploy.Name), + Status: status, + Reason: reason, + } + + mergeAppStatus(statusByApp, candidate) + } + + statefulSets, err := client.AppsV1().StatefulSets(metav1.NamespaceAll).List(ctx, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/managed-by=cloudness", + }) + if err != nil { + return nil, err + } + + for _, sts := range statefulSets.Items { + appUID := m.getApplicationUIDFromPodLabels(sts.Labels) + projectID := m.getProjectIDFromPodLabels(sts.Labels) + updatedAt := m.getUpdateTimeFromPodAnnotations(sts.Annotations) + + if appUID == 0 { + continue + } + if updatedAt != 0 && updatedAt > (nowMilli-10_000) { + continue + } + + status, reason := evaluateStatefulSetStatus(sts) + candidate := &types.AppStatus{ + Timestamp: now, + ApplicationUID: appUID, + ProjectID: projectID, + InstanceName: getApplicationNameFromLabels(sts.Labels, sts.Name), + Status: status, + Reason: reason, + } + + mergeAppStatus(statusByApp, candidate) + } + + pods, err := client.CoreV1().Pods(metav1.NamespaceAll).List(ctx, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/managed-by=cloudness", + }) + if err != nil { + return nil, err + } + + for _, pod := range pods.Items { + appUID := m.getApplicationUIDFromPodLabels(pod.Labels) + projectID := m.getProjectIDFromPodLabels(pod.Labels) + updatedAt := m.getUpdateTimeFromPodAnnotations(pod.Annotations) + + if appUID == 0 { + continue + } + if updatedAt != 0 && updatedAt > (nowMilli-10_000) { + continue + } + + status, reason := evaluatePodStatus(pod) + + candidate := &types.AppStatus{ + Timestamp: now, + ApplicationUID: appUID, + ProjectID: projectID, + InstanceName: getApplicationNameFromLabels(pod.Labels, pod.Name), + Status: status, + Reason: reason, + } + + mergeAppStatus(statusByApp, candidate) + } + + statuses := make([]*types.AppStatus, 0, len(statusByApp)) + for _, s := range statusByApp { + statuses = append(statuses, s) + } + + return statuses, nil +} + +func evaluatePodStatus(pod corev1.Pod) (enum.ApplicationStatus, string) { + // Check terminal phases first. + switch pod.Status.Phase { + case corev1.PodFailed: + return enum.ApplicationStatusError, firstNonEmpty(pod.Status.Reason, pod.Status.Message, "pod failed") + case corev1.PodUnknown: + return enum.ApplicationStatusError, firstNonEmpty(pod.Status.Reason, pod.Status.Message, "pod unknown") + case corev1.PodSucceeded: + return enum.ApplicationStatusSleeping, "pod succeeded" + } + + // For Pending and Running pods, inspect container-level statuses to detect + // errors such as ImagePullBackOff, CrashLoopBackOff, etc. that the phase + // alone does not surface. + + // Check init containers first — a failing init container blocks the whole pod. + if status, reason, found := evaluateContainerStatuses(pod.Status.InitContainerStatuses); found { + return status, reason + } + + // Then check regular containers. + if status, reason, found := evaluateContainerStatuses(pod.Status.ContainerStatuses); found { + return status, reason + } + + // No container-level error detected — fall back to phase. + if pod.Status.Phase == corev1.PodPending { + return enum.ApplicationStatusPaused, firstNonEmpty(pod.Status.Reason, pod.Status.Message, "pod pending") + } + + return enum.ApplicationStatusRunning, "" +} + +// evaluateContainerStatuses inspects a slice of container statuses and returns +// an error status if any container is in a bad state. The third return value +// indicates whether a conclusive status was found. +func evaluateContainerStatuses(statuses []corev1.ContainerStatus) (enum.ApplicationStatus, string, bool) { + for _, cs := range statuses { + if cs.State.Waiting != nil { + reason := firstNonEmpty(cs.State.Waiting.Reason, cs.State.Waiting.Message, "container waiting") + return enum.ApplicationStatusError, reason, true + } + + if cs.State.Terminated != nil && cs.State.Terminated.ExitCode != 0 { + reason := firstNonEmpty(cs.State.Terminated.Reason, cs.State.Terminated.Message, "container terminated") + return enum.ApplicationStatusError, reason, true + } + + if !cs.Ready && cs.State.Running != nil { + return enum.ApplicationStatusError, "container not ready", true + } + } + + return "", "", false +} + +func firstNonEmpty(values ...string) string { + for _, v := range values { + if v != "" { + return v + } + } + return "" +} + +func evaluateDeploymentStatus(deploy appsv1.Deployment) (enum.ApplicationStatus, string) { + desired := int32(1) + // if deploy.Spec.Replicas != nil { + // desired = *deploy.Spec.Replicas + // } + + // if desired == 0 { + // return enum.ApplicationStatusSleeping, "scaled to zero" + // } + + for _, cond := range deploy.Status.Conditions { + if cond.Type == appsv1.DeploymentProgressing && cond.Status == corev1.ConditionFalse { + return enum.ApplicationStatusError, firstNonEmpty(cond.Reason, cond.Message, "deployment not progressing") + } + if cond.Type == appsv1.DeploymentReplicaFailure && cond.Status == corev1.ConditionTrue { + return enum.ApplicationStatusError, firstNonEmpty(cond.Reason, cond.Message, "deployment replica failure") + } + } + + if deploy.Status.ReadyReplicas < desired { + return enum.ApplicationStatusPaused, fmt.Sprintf("ready replicas %d/%d", deploy.Status.ReadyReplicas, desired) + } + + if deploy.Status.UpdatedReplicas < desired { + return enum.ApplicationStatusPaused, fmt.Sprintf("updated replicas %d/%d", deploy.Status.UpdatedReplicas, desired) + } + + return enum.ApplicationStatusRunning, "" +} + +func evaluateStatefulSetStatus(sts appsv1.StatefulSet) (enum.ApplicationStatus, string) { + desired := int32(1) + // if sts.Spec.Replicas != nil { + // desired = *sts.Spec.Replicas + // } + + // if desired == 0 { + // return enum.ApplicationStatusSleeping, "scaled to zero" + // } + + for _, cond := range sts.Status.Conditions { + if cond.Status == corev1.ConditionFalse { + return enum.ApplicationStatusError, firstNonEmpty(cond.Reason, cond.Message, "statefulset condition failed") + } + } + + if sts.Status.ReadyReplicas < desired { + return enum.ApplicationStatusPaused, fmt.Sprintf("ready replicas %d/%d", sts.Status.ReadyReplicas, desired) + } + + if sts.Status.CurrentReplicas < desired { + return enum.ApplicationStatusPaused, fmt.Sprintf("current replicas %d/%d", sts.Status.CurrentReplicas, desired) + } + + return enum.ApplicationStatusRunning, "" +} + +func mergeAppStatus(statusByApp map[int64]*types.AppStatus, candidate *types.AppStatus) { + current, found := statusByApp[candidate.ApplicationUID] + if !found || isHigherPriorityStatus(candidate.Status, current.Status) { + statusByApp[candidate.ApplicationUID] = candidate + return + } + + if current.Status == candidate.Status && current.Reason == "" && candidate.Reason != "" { + current.Reason = candidate.Reason + } +} + +func isHigherPriorityStatus(candidate enum.ApplicationStatus, current enum.ApplicationStatus) bool { + priority := map[enum.ApplicationStatus]int{ + enum.ApplicationStatusError: 3, + enum.ApplicationStatusPaused: 2, + enum.ApplicationStatusRunning: 1, + enum.ApplicationStatusSleeping: 0, + } + + return priority[candidate] > priority[current] +} + +func getApplicationNameFromLabels(labels map[string]string, fallback string) string { + if name := labels["app.kubernetes.io/name"]; name != "" { + return name + } + + if name := labels["app.kubernetes.io/instance"]; name != "" { + return name + } + + if name := labels["app"]; name != "" { + return name + } + + return fallback +} diff --git a/app/services/manager/manager.go b/app/services/manager/manager.go index fc10278..bfe1dad 100644 --- a/app/services/manager/manager.go +++ b/app/services/manager/manager.go @@ -35,4 +35,7 @@ type ServerManager interface { //Metrics ListMetrics(ctx context.Context, server *types.Server) ([]*types.AppMetrics, error) + + //Application status + ListApplicationStatuses(ctx context.Context, server *types.Server) ([]*types.AppStatus, error) } diff --git a/app/store/database.go b/app/store/database.go index 834bf4d..50477fc 100644 --- a/app/store/database.go +++ b/app/store/database.go @@ -274,7 +274,7 @@ type ( UpdateDeploymentStatus(ctx context.Context, application *types.Application) (*types.Application, error) // UpdateStatus updates application status - UpdateStatus(ctx context.Context, application *types.Application) (*types.Application, error) + UpdateStatus(ctx context.Context, appUID int64, status enum.ApplicationStatus) (*types.Application, error) // UpdateDeploymentTriggerTime updates the application to latest trigger time UpdateDeploymentTriggerTime(ctx context.Context, application *types.Application) (*types.Application, error) diff --git a/app/store/database/application.go b/app/store/database/application.go index 53fd9c9..4e3601a 100644 --- a/app/store/database/application.go +++ b/app/store/database/application.go @@ -156,13 +156,13 @@ func (s *ApplicationStore) UpdateDeploymentStatus(ctx context.Context, applicati return s.update(ctx, application, applicationUpdateDeploymentStatus) } -func (s *ApplicationStore) UpdateStatus(ctx context.Context, application *types.Application) (*types.Application, error) { +func (s *ApplicationStore) UpdateStatus(ctx context.Context, appUID int64, status enum.ApplicationStatus) (*types.Application, error) { const applicationUpdateStatus = `UPDATE applications SET application_status = :application_status - WHERE application_id = :application_id` + WHERE application_uid = :application_uid` - return s.update(ctx, application, applicationUpdateStatus) + return s.update(ctx, &types.Application{UID: appUID, Status: status}, applicationUpdateStatus) } func (s *ApplicationStore) UpdateDeploymentTriggerTime(ctx context.Context, application *types.Application) (*types.Application, error) { diff --git a/app/web/handler/project/events.go b/app/web/handler/project/events.go index 9342e73..668631d 100644 --- a/app/web/handler/project/events.go +++ b/app/web/handler/project/events.go @@ -54,13 +54,23 @@ func renderFunc(ctx context.Context, w io.Writer, event *sse.Event) error { log.Ctx(ctx).Err(err).Msg("sse render: error rendering deployment full status") return err } - case enum.SSETypeApplicationUpdated: + case enum.SSETypeApplicationDeploymentUpdated: d := new(types.Application) if err := json.Unmarshal(event.Data, d); err != nil { log.Ctx(ctx).Err(err).Msg("sse render: error unmarshalling application") return err } - if err := vapplication.AppStatus(d, true).Render(ctx, w); err != nil { + if err := vapplication.AppDeploymentStatus(d, true).Render(ctx, w); err != nil { + log.Ctx(ctx).Err(err).Msg("sse render: error rendering application status") + return err + } + case enum.SSETypeApplicationStatusUpdated: + s := new(types.AppStatusEvent) + if err := json.Unmarshal(event.Data, s); err != nil { + log.Ctx(ctx).Err(err).Msg("sse render: error unmarshalling application status") + return err + } + if err := vapplication.AppStatus(s.ApplicationUID, s.Status, true).Render(ctx, w); err != nil { log.Ctx(ctx).Err(err).Msg("sse render: error rendering application status") return err } diff --git a/app/web/views/components/vapplication/app_status.templ b/app/web/views/components/vapplication/app_status.templ new file mode 100644 index 0000000..37aedcf --- /dev/null +++ b/app/web/views/components/vapplication/app_status.templ @@ -0,0 +1,28 @@ +package vapplication + +import "fmt" +import "github.com/cloudness-io/cloudness/types/enum" + +func getAppStatusID(appUID int64) string { + return fmt.Sprintf("app-status-%d", appUID) +} + +templ AppStatus(appUID int64, status enum.ApplicationStatus, swap bool) { +
+ @appStatusBadge(appUID, status) +
+} + +templ appStatusBadge(appUID int64, status enum.ApplicationStatus) { + +

{ string(status) }

+
+} diff --git a/app/web/views/components/vapplication/status.templ b/app/web/views/components/vapplication/deployment_status.templ similarity index 75% rename from app/web/views/components/vapplication/status.templ rename to app/web/views/components/vapplication/deployment_status.templ index 2661796..62b3ac8 100644 --- a/app/web/views/components/vapplication/status.templ +++ b/app/web/views/components/vapplication/deployment_status.templ @@ -6,7 +6,7 @@ import "github.com/cloudness-io/cloudness/types/enum" import "github.com/cloudness-io/cloudness/app/web/views/components/icons" import "github.com/cloudness-io/cloudness/app/web/views/shared" -func getAttributes(swap bool) templ.Attributes { +func getSwapAttributes(swap bool) templ.Attributes { if swap { return templ.Attributes{ "hx-swap-oob": "true", @@ -14,24 +14,25 @@ func getAttributes(swap bool) templ.Attributes { } return templ.Attributes{} } -func getAppStatusID(app *types.Application) string { - return fmt.Sprintf("app-status-%d", app.UID) + +func getAppDeploymentStatusID(app *types.Application) string { + return fmt.Sprintf("app-deployment-status-%d", app.UID) } -templ AppStatus(app *types.Application, swap bool) { -
+templ AppDeploymentStatus(app *types.Application, swap bool) { +
switch true { case app.Updated > app.DeploymentTriggeredAt, app.DeploymentStatus == "" , app.DeploymentStatus == enum.ApplicationDeploymentStatusNeedsDeployment: - @statusBadge(app, enum.ApplicationDeploymentStatusNeedsDeployment) + @deploymentStatusBadge(app, enum.ApplicationDeploymentStatusNeedsDeployment) default: - @statusBadge(app, app.DeploymentStatus) + @deploymentStatusBadge(app, app.DeploymentStatus) }
} -templ statusBadge(app *types.Application, status enum.ApplicationDeploymentStatus) { +templ deploymentStatusBadge(app *types.Application, status enum.ApplicationDeploymentStatus) {
Application |
+
Status:
+ @AppStatus(app.UID, app.Status, false) + |
Deployment Status:
- @AppStatus(app, false) + @AppDeploymentStatus(app, false)
diff --git a/app/web/views/components/vapplication/list.templ b/app/web/views/components/vapplication/list.templ index 4bb6952..6b9b759 100644 --- a/app/web/views/components/vapplication/list.templ +++ b/app/web/views/components/vapplication/list.templ @@ -57,7 +57,7 @@ templ list(env *types.Environment, apps []*types.Application) { } @shared.TableBodyCell() { - @AppStatus(app, false) + @AppDeploymentStatus(app, false) } } } diff --git a/types/app_status.go b/types/app_status.go new file mode 100644 index 0000000..3e2db18 --- /dev/null +++ b/types/app_status.go @@ -0,0 +1,32 @@ +package types + +import ( + "time" + + "github.com/cloudness-io/cloudness/types/enum" +) + +type AppStatus struct { + Timestamp time.Time `json:"timestamp"` + ApplicationUID int64 `json:"application_uid"` + ProjectID int64 `json:"project_id"` + InstanceName string `json:"instance_name"` + Status enum.ApplicationStatus `json:"status"` + Reason string `json:"reason"` +} + +func (s *AppStatus) ToEvent() *AppStatusEvent { + return &AppStatusEvent{ + ApplicationUID: s.ApplicationUID, + ProjectID: s.ProjectID, + Status: s.Status, + Reason: s.Reason, + } +} + +type AppStatusEvent struct { + ApplicationUID int64 `json:"application_uid"` + ProjectID int64 `json:"project_id"` + Status enum.ApplicationStatus `json:"status"` + Reason string `json:"reason"` +} diff --git a/types/enum/sse.go b/types/enum/sse.go index 1ef0f30..832f915 100644 --- a/types/enum/sse.go +++ b/types/enum/sse.go @@ -3,6 +3,7 @@ package enum type SSEType string const ( - SSETypeApplicationUpdated SSEType = "application_updated" - SSETypeDeploymentUpdated SSEType = "deployment_updated" + SSETypeApplicationDeploymentUpdated SSEType = "application_deployment_updated" + SSETypeApplicationStatusUpdated SSEType = "application_status_updated" + SSETypeDeploymentUpdated SSEType = "deployment_updated" ) From 0d1cf8c889d89f2a083ebfd550e5120c86b5b967 Mon Sep 17 00:00:00 2001 From: KArtHiK Date: Thu, 12 Feb 2026 14:33:09 +0100 Subject: [PATCH 2/2] Add deployment status to app list --- app/web/views/components/vapplication/list.templ | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/web/views/components/vapplication/list.templ b/app/web/views/components/vapplication/list.templ index 6b9b759..66a3df2 100644 --- a/app/web/views/components/vapplication/list.templ +++ b/app/web/views/components/vapplication/list.templ @@ -30,7 +30,7 @@ templ List(env *types.Environment, apps []*types.Application) { templ list(env *types.Environment, apps []*types.Application) {
@shared.Table() { - @shared.TableHeadFull("Name", "Live URL", "Status") + @shared.TableHeadFull("Name", "Live URL", "Status", "Deployment Status") @shared.TableBody() { for _,app := range apps { @shared.TableBodyRow(templ.Attributes{ @@ -56,6 +56,9 @@ templ list(env *types.Environment, apps []*types.Application) { } } + @shared.TableBodyCell() { + @AppStatus(app.UID, app.Status, false) + } @shared.TableBodyCell() { @AppDeploymentStatus(app, false) }