Skip to content

feat(monitoring): alert on ECS services that cannot start tasks - #160

Merged
Alexanderamiri merged 2 commits into
mainfrom
feat/ecs-impaired-alert
Oct 26, 2026
Merged

Alexanderamiri merged 2 commits into
mainfrom
feat/ecs-impaired-alert

Conversation

@Alexanderamiri

Copy link
Copy Markdown
Member

Why

testteam-platform-test-app has been crash-looping for weeks with desired=1 and running=0, because of a CannotPullContainerError (its ECR repo is empty). That is about 80 failed task starts a day, and nobody noticed. Each attempt creates and deletes a Fargate ENI, which AWS Config records. This was the main driver of about $29/mo in Config charges found in the 2026-10 cost audit. PR #159 reduces the Config side. This PR makes the crash loop itself visible.

What

  • monitoring/main.tf: new EventBridge rule javabin-ecs-task-start-impaired. It matches source=aws.ecs, detail-type=ECS Service Action and eventName=SERVICE_TASK_START_IMPAIRED, for the javabin-platform cluster only (legacy CDK clusters are excluded). The target is the javabin-security SNS topic, whose policy already allows events.amazonaws.com. slack-alert sends non-finding events from that topic to #javabin-infra-alerts. I chose that channel over the cost channel because this is an operational problem.
  • slack_alert/handler.py:
    • New format_ecs_service_impaired formatter, routed before the generic EventBridge branch. Without it, the event would fall through to the IAM-change formatter.
    • The message shows the service, the cluster and a link to the service's events page in the ECS console.
    • Dedup uses the existing persistent javabin-alert-dedup table, keyed per service ARN per UTC day. ECS keeps emitting the event while the service is impaired.
  • New test terraform/lambda-src/tests/test_slack_alert_ecs.py checks the formatting, once-per-day dedup per service, and that other ECS service actions are ignored.
  • Docs: added a line to the CLAUDE.md alert routing section and to docs/lambda-functions.md.

Expected plan

2 to add: aws_cloudwatch_event_rule.ecs_task_start_impaired and aws_cloudwatch_event_target.ecs_task_start_impaired_sns. 1 in-place update: the slack-alert Lambda code. No IAM changes, because slack-alert already has GetItem/PutItem on the dedup table.

How verified

  • python3 -m unittest discover -s terraform/lambda-src/tests (67 OK, 3 new) and -s scripts/tests pass.
  • terraform fmt -recursive -check, then terraform init -backend=false and terraform validate pass.

Follow-ups

🤖 Generated with Claude Code

@Alexanderamiri
Alexanderamiri requested a review from a team as a code owner October 24, 2026 13:11
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 2 to add, 2 to change, 0 to destroy.

Plan output
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place

Terraform will perform the following actions:

  # module.lambdas.aws_lambda_function.securityhub_summary will be updated in-place
  ~ resource "aws_lambda_function" "securityhub_summary" {
        id                             = "javabin-securityhub-summary"
      ~ last_modified                  = "2026-10-23T09:00:34.000+0000" -> (known after apply)
      ~ source_code_hash               = "hrcvTHhG+KIWoQVwbPKmZHx4p1RGl3VZPkTdSdhyfKk=" -> "ci2qbjUrBx0+yEslzlkKpQdZ0eQltqrBbHoakTuR6Ik="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.slack_alert will be updated in-place
  ~ resource "aws_lambda_function" "slack_alert" {
        id                             = "javabin-slack-alert"
      ~ last_modified                  = "2026-10-23T09:00:40.000+0000" -> (known after apply)
      ~ source_code_hash               = "hrcvTHhG+KIWoQVwbPKmZHx4p1RGl3VZPkTdSdhyfKk=" -> "ci2qbjUrBx0+yEslzlkKpQdZ0eQltqrBbHoakTuR6Ik="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.ecs_task_start_impaired will be created
  + resource "aws_cloudwatch_event_rule" "ecs_task_start_impaired" {
      + arn            = (known after apply)
      + description    = "ECS services in the platform cluster that cannot start tasks"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + clusterArn = [
                      + "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                    ]
                  + eventName  = [
                      + "SERVICE_TASK_START_IMPAIRED",
                    ]
                }
              + detail-type = [
                  + "ECS Service Action",
                ]
              + source      = [
                  + "aws.ecs",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-ecs-task-start-impaired"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.monitoring.aws_cloudwatch_event_target.ecs_task_start_impaired_sns will be created
  + resource "aws_cloudwatch_event_target" "ecs_task_start_impaired_sns" {
      + arn            = "arn:aws:sns:eu-central-1:553637109631:javabin-security"
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + rule           = "javabin-ecs-task-start-impaired"
      + target_id      = "send-to-security-sns"
    }

Plan: 2 to add, 2 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟢 LOW

Plan updates two Lambda functions' code (source hash change only) and adds a CloudWatch Event rule and SNS target to alert on ECS task start impairment in the platform cluster; 0 destroys.

  • ✅ [routine] aws_lambda_function.securityhub_summary is updated in-place: only source_code_hash changes (new deployment package). Configuration, role and other attributes are unchanged; no replacement or destroy.
  • ✅ [routine] aws_lambda_function.slack_alert is updated in-place with the same source_code_hash change as securityhub_summary (identical hash, likely a shared package). No configuration changes visible.
  • 🔒 [security] New EventBridge rule matches SERVICE_TASK_START_IMPAIRED events for the javabin-platform ECS cluster and routes them to the javabin-security SNS topic. This is an alerting addition, not an exposure: it adds no public access, IAM grants or security group changes. Confirm the SNS topic's access policy permits EventBridge to publish.
  • 💰 [cost] Minimal cost: one EventBridge rule and one SNS target. Event volume should be low, and no new billable infrastructure such as NAT gateways, RDS or load balancers is created.
  • ✅ [routine] Target uses rule name 'javabin-ecs-task-start-impaired' as a literal string rather than a reference to the rule resource. This works if the name matches, but it is fragile if the rule is renamed.

@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 2 to add, 2 to change, 0 to destroy.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place

Terraform will perform the following actions:

  # module.lambdas.aws_lambda_function.securityhub_summary will be updated in-place
  ~ resource "aws_lambda_function" "securityhub_summary" {
        id                             = "javabin-securityhub-summary"
      ~ last_modified                  = "2026-10-23T09:00:34.000+0000" -> (known after apply)
      ~ source_code_hash               = "hrcvTHhG+KIWoQVwbPKmZHx4p1RGl3VZPkTdSdhyfKk=" -> "ci2qbjUrBx0+yEslzlkKpQdZ0eQltqrBbHoakTuR6Ik="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.slack_alert will be updated in-place
  ~ resource "aws_lambda_function" "slack_alert" {
        id                             = "javabin-slack-alert"
      ~ last_modified                  = "2026-10-23T09:00:40.000+0000" -> (known after apply)
      ~ source_code_hash               = "hrcvTHhG+KIWoQVwbPKmZHx4p1RGl3VZPkTdSdhyfKk=" -> "ci2qbjUrBx0+yEslzlkKpQdZ0eQltqrBbHoakTuR6Ik="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.ecs_task_start_impaired will be created
  + resource "aws_cloudwatch_event_rule" "ecs_task_start_impaired" {
      + arn            = (known after apply)
      + description    = "ECS services in the platform cluster that cannot start tasks"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + clusterArn = [
                      + "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                    ]
                  + eventName  = [
                      + "SERVICE_TASK_START_IMPAIRED",
                    ]
                }
              + detail-type = [
                  + "ECS Service Action",
                ]
              + source      = [
                  + "aws.ecs",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-ecs-task-start-impaired"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.monitoring.aws_cloudwatch_event_target.ecs_task_start_impaired_sns will be created
  + resource "aws_cloudwatch_event_target" "ecs_task_start_impaired_sns" {
      + arn            = "arn:aws:sns:eu-central-1:553637109631:javabin-security"
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + rule           = "javabin-ecs-task-start-impaired"
      + target_id      = "send-to-security-sns"
    }

Plan: 2 to add, 2 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟢 LOW

Plan updates two existing Lambda functions (code-only source_code_hash change) and adds an EventBridge rule and target that forward ECS task-start-impaired events from the javabin-platform cluster to the javabin-security SNS topic; nothing is destroyed.

  • ✅ [routine] Two in-place updates to aws_lambda_function.securityhub_summary and aws_lambda_function.slack_alert. Only source_code_hash and last_modified change, so this is a code redeploy with no configuration, IAM role, or environment changes visible in the plan.
  • ✅ [routine] New EventBridge rule javabin-ecs-task-start-impaired matches aws.ecs SERVICE_TASK_START_IMPAIRED events for the javabin-platform cluster only. It is scoped to a single cluster ARN and has no public exposure.
  • ✅ [routine] New EventBridge target sends matched events to the existing SNS topic javabin-security. Confirm the topic's access policy allows events.amazonaws.com to publish; this is normally already handled by the topic policy or the target setup.
  • 💰 [cost] Negligible cost impact. EventBridge rule matches and SNS publishes are billed per event and only occur when ECS services fail to start tasks.
  • 💥 [destruction] No resources are destroyed (0 to destroy). The Lambda updates replace code in place, not the function resources.

Alexanderamiri added a commit that referenced this pull request Oct 26, 2026
… control (#159)

## Why

A cost audit (2026-10, live Cost Explorer/CloudWatch/Config data) found
that the cost guardrails we already run have gaps:

- **The account budget fires every month.** It is $500, but steady spend
is about $550 a month (including 25% VAT), and the ACTUAL 80%/100%
alerts trigger on that. There is no forecast alert.
- **`javabin-billing-alarm` (us-east-1) does nothing.** It has been
`INSUFFICIENT_DATA` since 2026-03-26 because the AWS/Billing
EstimatedCharges metric is not enabled for the account. Its SNS topic
has 0 subscribers, and its $200 threshold is below monthly spend anyway.
- **AWS Config churn.** Over 30 days Config recorded about 12.6k
configuration items ($0.003 each): NetworkInterface 2451, VPC 2446,
Subnet 2446, SecurityGroup 2445. ENI + VPC + Subnet is about 7.3k items
(~$22). Each Fargate task start/stop re-records the ENI and its
relationships. Almost all of it comes from `testteam-platform-test-app`
crash-looping (~80 starts/day), so **fixing that service is the main
saving**; this PR only trims what remains.
- **The 15 `/aws/lambda/javabin-*` log groups and auth-claims never
expire.** Lambda auto-created them, and Terraform doesn't manage them.
- **The Config delivery bucket has no lifecycle rule.**

## What

- `cost-analytics`:
  - Budget raised to $600.
- Notifications are now FORECASTED >100%, ACTUAL >100% and ACTUAL >150%,
all to `javabin-alerts`, which the slack-alert Lambda posts to
#javabin-cost-alerts.
- Removed `aws_cloudwatch_metric_alarm.billing`,
`aws_sns_topic.billing_alarm` and the `billing_alarm_threshold_usd`
variable. The us-east-1 provider alias stays because the CUR export uses
it.
- `monitoring`:
- Config recorder still records all supported types, but
`AWS::EC2::NetworkInterface`, `AWS::EC2::VPC` and `AWS::EC2::Subnet` use
a DAILY override. Security groups stay CONTINUOUS. ENIs are deliberately
**not** excluded: Security Hub **EC2.180** (ENI source/destination
check, enabled) evaluates `AWS::EC2::NetworkInterface`, and **Config.1**
fails when an enabled control's resource type is not recorded.
Trade-off: EC2.180 evaluates daily instead of on every change, and an
ENI created and deleted within one day may never be recorded. Expected
saving ~$15–20/mo while the crash loop lasts, a few dollars a month
after it is fixed.
- The `javabin-config-<acct>` bucket now expires objects after 730 days.
The bucket is not versioned.
- `lambdas/log-groups.tf` and `identity/log-groups.tf`:
`aws_cloudwatch_log_group` with 365-day retention for every platform
Lambda.
- `imports.tf`: `for_each` import blocks (TF 1.7) adopt the 14 existing
groups plus auth-claims. budget-enforcer has never logged, so its group
is created fresh.
- `scripts/tests/test_lambda_log_groups.py`: scans every
`terraform/platform/**/*.tf` and fails if a `${var.project}-*` Lambda
has no log group (shared `local.platform_lambda_names` or an explicit
`/aws/lambda/...` group in its module).
- Docs:
- New `docs/cost-management.md` covering alerts, tags, CUR, built-in
controls and why Config records ENIs daily, and the post-apply checks.
- Updated CLAUDE.md (file table, a separate account-budget alert-routing
line so it doesn't conflict with #160, stale cost-allocation-tag note),
`docs/platform-modules.md` and `docs/lambda-functions.md`.

Not changed, on purpose:
- The anomaly monitor (SERVICE, $10 IMMEDIATE) is adequate.
- Cost-allocation tags are already Active.
- ECR lifecycle and app log retention (30 days) already exist.

## Expected plan

- `module.cost_analytics.aws_budgets_budget.account`: updated in place.
- `module.cost_analytics.aws_cloudwatch_metric_alarm.billing` and
`aws_sns_topic.billing_alarm`: **destroyed**. Both are inert, as
explained above.
- `module.monitoring.aws_config_configuration_recorder.main`: updated in
place.
- `module.monitoring.aws_s3_bucket_lifecycle_configuration.config_logs`:
created.
- 15 log groups imported (retention updated from never to 365 days) and
1 created (budget-enforcer).

## How verified

- `terraform fmt -recursive -check`, then `terraform init
-backend=false` and `terraform validate` in `terraform/platform`. All
pass.
- `python3 -m unittest discover -s scripts/tests` (46 OK) and `-s
terraform/lambda-src/tests` (OK). Negative check: renaming the
auth-claims log group makes the guard test fail.
- `git merge-tree` against `feat/ecs-impaired-alert` (#160): no
conflict.
- Live: Config.1 currently FAILED for IAM global types + custom recorder
role (pre-existing); EC2.180 rule scoped to
`AWS::EC2::NetworkInterface`.
- Live checks: the `javabin-config-*` bucket is unversioned with no
lifecycle, the recorder is `ALL_SUPPORTED_RESOURCE_TYPES` + CONTINUOUS,
and the list of existing `/aws/lambda/javabin-*` log groups matches the
import list.

## Follow-ups

- 7 days after apply:
- Check `AWS/Config ConfigurationItemsRecorded` per ResourceType. If
SecurityGroup still dominates, add it to the DAILY override.
  - Security Hub EC2.180 still gets fresh evaluations (not NO_DATA).
- Security Hub Config.1 has no new reason naming
`AWS::EC2::NetworkInterface`.
- Scale `testteam-platform-test-app` to 0, or re-push its image. It is
the root cause of the Config churn.
- Lower `account_budget_usd` as the legacy cleanup lands. The cleanup is
in the cost audit report: idle EB environments, Default-ALB, Coolify,
NAT.
- The import blocks can be dropped after the first apply.
- If the budget-enforcer Lambda runs before this is applied, its log
group will already exist and the create will fail. In that case, add it
to the import list.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Alexanderamiri and others added 2 commits October 26, 2026 15:16
EventBridge rule on SERVICE_TASK_START_IMPAIRED for the platform cluster,
routed through javabin-security to #javabin-infra-alerts. slack-alert
formats it and dedups once per service per UTC day in the alert-dedup table.
A crash-looping service otherwise burns Fargate starts, image pulls and AWS
Config items unnoticed (testteam-platform-test-app, ~80 attempts/day).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Alexanderamiri
Alexanderamiri force-pushed the feat/ecs-impaired-alert branch from 02d1507 to cc43e1d Compare October 26, 2026 14:16
@github-actions

Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 2 to add, 3 to change, 0 to destroy.

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place

Terraform will perform the following actions:

  # module.identity.aws_cognito_identity_provider.internal_google[0] will be updated in-place
  ~ resource "aws_cognito_identity_provider" "internal_google" {
        id                = "eu-central-1_Icikv3dtD:Google"
      ~ provider_details  = {
          ~ "authorize_url"                 = "https://accounts.google.com/o/oauth2/v2/auth" -> "https://accounts.google.com/o/oauth2/v2/auth?hd=java.no&prompt=select_account"
            # (8 unchanged elements hidden)
        }
        # (5 unchanged attributes hidden)
    }

  # module.lambdas.aws_lambda_function.securityhub_summary will be updated in-place
  ~ resource "aws_lambda_function" "securityhub_summary" {
        id                             = "javabin-securityhub-summary"
      ~ last_modified                  = "2026-10-23T09:00:34.000+0000" -> (known after apply)
      ~ source_code_hash               = "hrcvTHhG+KIWoQVwbPKmZHx4p1RGl3VZPkTdSdhyfKk=" -> "ci2qbjUrBx0+yEslzlkKpQdZ0eQltqrBbHoakTuR6Ik="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.lambdas.aws_lambda_function.slack_alert will be updated in-place
  ~ resource "aws_lambda_function" "slack_alert" {
        id                             = "javabin-slack-alert"
      ~ last_modified                  = "2026-10-23T09:00:40.000+0000" -> (known after apply)
      ~ source_code_hash               = "hrcvTHhG+KIWoQVwbPKmZHx4p1RGl3VZPkTdSdhyfKk=" -> "ci2qbjUrBx0+yEslzlkKpQdZ0eQltqrBbHoakTuR6Ik="
        tags                           = {}
        # (21 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.monitoring.aws_cloudwatch_event_rule.ecs_task_start_impaired will be created
  + resource "aws_cloudwatch_event_rule" "ecs_task_start_impaired" {
      + arn            = (known after apply)
      + description    = "ECS services in the platform cluster that cannot start tasks"
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + clusterArn = [
                      + "arn:aws:ecs:eu-central-1:553637109631:cluster/javabin-platform",
                    ]
                  + eventName  = [
                      + "SERVICE_TASK_START_IMPAIRED",
                    ]
                }
              + detail-type = [
                  + "ECS Service Action",
                ]
              + source      = [
                  + "aws.ecs",
                ]
            }
        )
      + force_destroy  = false
      + id             = (known after apply)
      + name           = "javabin-ecs-task-start-impaired"
      + name_prefix    = (known after apply)
      + tags_all       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "platform"
        }
    }

  # module.monitoring.aws_cloudwatch_event_target.ecs_task_start_impaired_sns will be created
  + resource "aws_cloudwatch_event_target" "ecs_task_start_impaired_sns" {
      + arn            = "arn:aws:sns:eu-central-1:553637109631:javabin-security"
      + event_bus_name = "default"
      + force_destroy  = false
      + id             = (known after apply)
      + rule           = "javabin-ecs-task-start-impaired"
      + target_id      = "send-to-security-sns"
    }

Plan: 2 to add, 3 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟢 LOW

Plan makes 2 additive changes (a CloudWatch EventBridge rule and target for ECS task start-impaired events forwarded to SNS) and 3 in-place updates (Google identity provider authorize URL gaining hd=java.no restriction and prompt parameter, and two Lambda code updates), with 0 destroys.

  • 🔒 [security] Cognito Google identity provider authorize_url now includes hd=java.no and prompt=select_account. This is a tightening change: the hd parameter restricts Google sign-in to java.no Google Workspace accounts and prompt=select_account forces account selection. It is not an opening to the internet, but confirm that java.no is the intended domain and that existing non-java.no users are not expected to sign in through this provider.
  • ✅ [routine] Both Lambda functions (securityhub_summary and slack_alert) have source_code_hash changes only, with the same new hash for both, meaning a code redeploy. Configuration, IAM role, and environment are unchanged in the visible diff. last_modified being known after apply is expected.
  • ✅ [routine] New EventBridge rule javabin-ecs-task-start-impaired matches SERVICE_TASK_START_IMPAIRED events from the javabin-platform ECS cluster and sends them to the javabin-security SNS topic. Purely alerting, no compute or data cost of note. Verify the SNS topic's access policy allows events.amazonaws.com to publish, otherwise the target will silently fail to deliver.
  • 💰 [cost] No new billable infrastructure of significance. EventBridge rule matches are negligible in cost and SNS publish charges are minimal.
  • 💥 [destruction] No resources are destroyed (0 to destroy). All changes are creates or in-place updates, so no data loss or force-replacement risk.

@Alexanderamiri
Alexanderamiri merged commit ccd5c3d into main Oct 26, 2026
4 checks passed
@Alexanderamiri
Alexanderamiri deleted the feat/ecs-impaired-alert branch October 26, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant