From d38d1b0c92818ce7011638b7729448056d7aaf31 Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:49:20 +0000 Subject: [PATCH 1/8] eli-420 updating workflow to use a custom 'reporting' environment --- .github/workflows/monthly-capacity-report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/monthly-capacity-report.yml b/.github/workflows/monthly-capacity-report.yml index 9d6febc69..ee0be9503 100644 --- a/.github/workflows/monthly-capacity-report.yml +++ b/.github/workflows/monthly-capacity-report.yml @@ -15,7 +15,7 @@ permissions: jobs: generate-report: runs-on: ubuntu-latest - environment: prod + environment: reporting steps: - name: Checkout code From 1d1f4ee71cd43a48de280991b21a729ecc24cfee Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:56:35 +0000 Subject: [PATCH 2/8] amending account id --- .github/workflows/monthly-capacity-report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/monthly-capacity-report.yml b/.github/workflows/monthly-capacity-report.yml index ee0be9503..748dd2aae 100644 --- a/.github/workflows/monthly-capacity-report.yml +++ b/.github/workflows/monthly-capacity-report.yml @@ -29,7 +29,7 @@ jobs: - name: "Configure AWS Credentials" uses: aws-actions/configure-aws-credentials@v5 with: - role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role + role-to-assume: arn:aws:iam::${{ secrets.AWS_PROD_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role aws-region: eu-west-2 - name: Generate dashboard report From df2ed9a55d786142da822447dc96c0b039fd5f31 Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Tue, 9 Dec 2025 14:27:19 +0000 Subject: [PATCH 3/8] eli-420 adding github permissions --- .github/workflows/monthly-capacity-report.yml | 2 +- .../stacks/iams-developer-roles/github_actions_policies.tf | 3 +++ .../stacks/iams-developer-roles/iams_permissions_boundary.tf | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/monthly-capacity-report.yml b/.github/workflows/monthly-capacity-report.yml index 748dd2aae..2200f357b 100644 --- a/.github/workflows/monthly-capacity-report.yml +++ b/.github/workflows/monthly-capacity-report.yml @@ -29,7 +29,7 @@ jobs: - name: "Configure AWS Credentials" uses: aws-actions/configure-aws-credentials@v5 with: - role-to-assume: arn:aws:iam::${{ secrets.AWS_PROD_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role + role-to-assume: arn:aws:iam::${{ secrets.AWS_DEV_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role aws-region: eu-west-2 - name: Generate dashboard report diff --git a/infrastructure/stacks/iams-developer-roles/github_actions_policies.tf b/infrastructure/stacks/iams-developer-roles/github_actions_policies.tf index ed9cd263b..8fe92f012 100644 --- a/infrastructure/stacks/iams-developer-roles/github_actions_policies.tf +++ b/infrastructure/stacks/iams-developer-roles/github_actions_policies.tf @@ -663,6 +663,8 @@ resource "aws_iam_policy" "cloudwatch_management" { "cloudwatch:ListTagsForResource", "cloudwatch:TagResource", "cloudwatch:UntagResource", + "cloudwatch:GetDashboard", + "cloudwatch:GetMetricWidgetImage", "sns:CreateTopic", "sns:DeleteTopic", @@ -683,6 +685,7 @@ resource "aws_iam_policy" "cloudwatch_management" { "arn:aws:logs:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:log-group:aws-wafv2-logs-*", "arn:aws:logs:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:log-group:aws-waf-logs-*", "arn:aws:cloudwatch:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:alarm:*", + "arn:aws:cloudwatch::${data.aws_caller_identity.current.account_id}:dashboard/Demand_And_Capacity_*", "arn:aws:sns:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:cloudwatch-security-alarms*", "arn:aws:logs:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/apigateway/default-eligibility-signposting-api*", ] diff --git a/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf b/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf index 9bfc6cfc9..ecda1d674 100644 --- a/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf +++ b/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf @@ -26,6 +26,8 @@ data "aws_iam_policy_document" "permissions_boundary" { "cloudwatch:ListTagsForResource", "cloudwatch:TagResource", "cloudwatch:UntagResource", + "cloudwatch:GetDashboard", + "cloudwatch:GetMetricWidgetImage", # DynamoDB - table management "dynamodb:DescribeTimeToLive", From f97c7ce6ea217b70090d9319ab6f0860fc165bfa Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:37:59 +0000 Subject: [PATCH 4/8] eli-420 adding back prod AWS to job (we can extend to other tiers) --- .github/workflows/monthly-capacity-report.yml | 2 +- .../iams-developer-roles/github_actions_policies.tf | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/monthly-capacity-report.yml b/.github/workflows/monthly-capacity-report.yml index 2200f357b..748dd2aae 100644 --- a/.github/workflows/monthly-capacity-report.yml +++ b/.github/workflows/monthly-capacity-report.yml @@ -29,7 +29,7 @@ jobs: - name: "Configure AWS Credentials" uses: aws-actions/configure-aws-credentials@v5 with: - role-to-assume: arn:aws:iam::${{ secrets.AWS_DEV_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role + role-to-assume: arn:aws:iam::${{ secrets.AWS_PROD_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role aws-region: eu-west-2 - name: Generate dashboard report diff --git a/infrastructure/stacks/iams-developer-roles/github_actions_policies.tf b/infrastructure/stacks/iams-developer-roles/github_actions_policies.tf index 8fe92f012..581225318 100644 --- a/infrastructure/stacks/iams-developer-roles/github_actions_policies.tf +++ b/infrastructure/stacks/iams-developer-roles/github_actions_policies.tf @@ -639,6 +639,8 @@ resource "aws_iam_policy" "firehose_readonly" { } resource "aws_iam_policy" "cloudwatch_management" { + #checkov:skip=CKV_AWS_355: GetMetricWidgetImage requires wildcard resource + #checkov:skip=CKV_AWS_290: GetMetricWidgetImage requires wildcard resource name = "cloudwatch-management" description = "Allow GitHub Actions to manage CloudWatch logs, alarms, and SNS topics" path = "/service-policies/" @@ -646,6 +648,14 @@ resource "aws_iam_policy" "cloudwatch_management" { policy = jsonencode({ Version = "2012-10-17", Statement = [ + { + Effect = "Allow", + Action = [ + # GetMetricWidgetImage does not support resource-level permissions + "cloudwatch:GetMetricWidgetImage" + ], + Resource = "*" + }, { Effect = "Allow", Action = [ @@ -664,7 +674,6 @@ resource "aws_iam_policy" "cloudwatch_management" { "cloudwatch:TagResource", "cloudwatch:UntagResource", "cloudwatch:GetDashboard", - "cloudwatch:GetMetricWidgetImage", "sns:CreateTopic", "sns:DeleteTopic", From 46102badd1b9668a5ad6f56adb33ff202329019e Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Sun, 14 Dec 2025 15:36:29 +0000 Subject: [PATCH 5/8] eli-420 improving format and layout of reports --- scripts/generate_dashboard_report.py | 203 ++++++++++++++++++--------- 1 file changed, 134 insertions(+), 69 deletions(-) diff --git a/scripts/generate_dashboard_report.py b/scripts/generate_dashboard_report.py index 611162673..515d8703e 100644 --- a/scripts/generate_dashboard_report.py +++ b/scripts/generate_dashboard_report.py @@ -7,6 +7,35 @@ from datetime import datetime from pathlib import Path +# Context definitions for widgets +WIDGET_CONTEXT = { + "DynamoDB": "Shows the consumed write capacity units for the DynamoDB table. High usage may indicate need for scaling.", + "Lambda": "Number of times the Lambda function was invoked. Spikes may indicate increased traffic or retries.", + "5xx": "Server-side errors. Should be zero ideally.", + "4xx": "Client-side errors. Frequent 4xx errors might indicate issues with client requests.", + "Latency": "Response time of the service. Lower is better.", + "CPU": "CPU utilization percentage. Consistently high CPU might require instance upsizing.", + "Memory": "Memory usage. Ensure there is sufficient headroom.", + "Errors": "Count of error events.", + "Throttles": "Number of throttled requests. Indicates capacity limits are being hit." +} + +def get_widget_description(title): + """ + Get a description for a widget based on keywords in its title. + """ + title_lower = title.lower() + description_parts = [] + + for key, desc in WIDGET_CONTEXT.items(): + if key.lower() in title_lower: + description_parts.append(desc) + + if not description_parts: + return "Performance metric visualization." + + return " ".join(description_parts) + def generate_html_report(images_dir='dashboard_exports', output_file=None): """ Generate an HTML report with all dashboard widget images. @@ -35,7 +64,7 @@ def generate_html_report(images_dir='dashboard_exports', output_file=None): # Get dashboard name and timestamp from definition file dashboard_name = "Monthly Demand And Capacity Report - EliD" - report_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + report_date = datetime.now().strftime('%d %B %Y at %H:%M') # Build HTML html_content = f""" @@ -43,8 +72,18 @@ def generate_html_report(images_dir='dashboard_exports', output_file=None): - Dashboard Report - {dashboard_name} + NHS Dashboard Report - {dashboard_name} -
-
-

šŸ“Š {dashboard_name}

-
Last 8 Weeks Report • Generated: {report_date}
+
+
+
+ +

{dashboard_name}

+
Generated on {report_date}
+
+
-
+
""" # Add each widget image @@ -167,27 +225,35 @@ def generate_html_report(images_dir='dashboard_exports', output_file=None): # Replace underscores with spaces title = title.replace('_', ' ') + description = get_widget_description(title) + # Read and encode image with open(image_file, 'rb') as f: image_data = base64.b64encode(f.read()).decode('utf-8') html_content += f""" -
+
+
{idx}. {title}
+
{description}
+
+
{title}
+
""" # Close HTML html_content += """ -
+
- + """ @@ -196,11 +262,10 @@ def generate_html_report(images_dir='dashboard_exports', output_file=None): with open(output_file, 'w', encoding='utf-8') as f: f.write(html_content) - print(f"\nāœ“ Report generated: {output_file}") + print(f"\nāœ“ Capacity and Demand Report generated: {output_file}") print(f"\nTo view:") print(f" - Open in browser: file://{Path(output_file).absolute()}") print(f" - Or run: xdg-open {output_file}") - print(f"\nTo save as PDF: Open in browser → Print → Save as PDF") return output_file From ac392a6acd4c467c4d1c0124f6355fe1de1a07a9 Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Sun, 14 Dec 2025 15:51:13 +0000 Subject: [PATCH 6/8] eli-420 updating report generation to get data from all environments --- .github/workflows/monthly-capacity-report.yml | 68 +++++++-- scripts/export_dashboard_image.sh | 15 +- scripts/generate_dashboard_report.py | 135 +++++++++++++----- 3 files changed, 161 insertions(+), 57 deletions(-) diff --git a/.github/workflows/monthly-capacity-report.yml b/.github/workflows/monthly-capacity-report.yml index 748dd2aae..81a5a88a5 100644 --- a/.github/workflows/monthly-capacity-report.yml +++ b/.github/workflows/monthly-capacity-report.yml @@ -26,26 +26,77 @@ jobs: with: python-version: "3.11" - - name: "Configure AWS Credentials" + # ---------------------------------------------------------------- + # 1. Export PROD + # ---------------------------------------------------------------- + - name: "Configure AWS Credentials (Prod)" uses: aws-actions/configure-aws-credentials@v5 with: role-to-assume: arn:aws:iam::${{ secrets.AWS_PROD_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role aws-region: eu-west-2 - - name: Generate dashboard report + - name: Export Dashboard (Prod) run: | chmod +x scripts/export_dashboard_image.sh - ./scripts/export_dashboard_image.sh Demand_And_Capacity_Prod + ./scripts/export_dashboard_image.sh Demand_And_Capacity_Prod Prod env: AWS_REGION: eu-west-2 + # ---------------------------------------------------------------- + # 2. Export PREPROD + # ---------------------------------------------------------------- + - name: "Configure AWS Credentials (Preprod)" + uses: aws-actions/configure-aws-credentials@v5 + with: + role-to-assume: arn:aws:iam::${{ secrets.AWS_PREPROD_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role + aws-region: eu-west-2 + + - name: Export Dashboard (Preprod) + run: ./scripts/export_dashboard_image.sh Demand_And_Capacity_Preprod Preprod + env: + AWS_REGION: eu-west-2 + + # ---------------------------------------------------------------- + # 3. Export TEST + # ---------------------------------------------------------------- + - name: "Configure AWS Credentials (Test)" + uses: aws-actions/configure-aws-credentials@v5 + with: + role-to-assume: arn:aws:iam::${{ secrets.AWS_TEST_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role + aws-region: eu-west-2 + + - name: Export Dashboard (Test) + run: ./scripts/export_dashboard_image.sh Demand_And_Capacity_Test Test + env: + AWS_REGION: eu-west-2 + + # ---------------------------------------------------------------- + # 4. Export DEV + # ---------------------------------------------------------------- + - name: "Configure AWS Credentials (Dev)" + uses: aws-actions/configure-aws-credentials@v5 + with: + role-to-assume: arn:aws:iam::${{ secrets.AWS_DEV_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role + aws-region: eu-west-2 + + - name: Export Dashboard (Dev) + run: ./scripts/export_dashboard_image.sh Demand_And_Capacity_Dev Dev + env: + AWS_REGION: eu-west-2 + + # ---------------------------------------------------------------- + # Generate & Notify + # ---------------------------------------------------------------- + - name: Generate Combined Report + run: python3 scripts/generate_dashboard_report.py --input dashboard_exports + - name: Upload report as artifact uses: actions/upload-artifact@v4 with: name: capacity-report path: | - dashboard_exports/*.html - dashboard_exports/*.png + dashboard_exports/**/*.html + dashboard_exports/**/*.png retention-days: 90 - name: Send to Slack @@ -54,21 +105,20 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_D_AND_C_WEBHOOK }} run: | # Get the latest HTML report - REPORT_FILE=$(ls -t dashboard_exports/dashboard_report_*.html | head -1) + REPORT_FILE=$(find dashboard_exports -name "dashboard_report_*.html" | head -n 1) REPORT_NAME=$(basename "$REPORT_FILE") # GitHub Actions URL GITHUB_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" - # Send Slack notification with simple variables for Workflow Automation + # Send Slack notification curl -X POST "$SLACK_WEBHOOK_URL" \ -H 'Content-Type: application/json' \ -d @- <No data found for {env_name}

" - # Find all PNG images image_files = sorted(images_path.glob('*.png')) - if not image_files: - print(f"Error: No PNG images found in {images_dir}") - return None + return f"

No images found for {env_name}

" - print(f"Found {len(image_files)} images to include in report") + html = f""" +
+

{env_name}

+ """ + + for idx, image_file in enumerate(image_files, 1): + filename = image_file.stem + title = filename.split('_', 1)[1] if '_' in filename else filename + title = title.replace('_', ' ') + description = get_widget_description(title) + + with open(image_file, 'rb') as f: + image_data = base64.b64encode(f.read()).decode('utf-8') + + html += f""" +
+
+
{idx}. {title}
+
{description}
+
+
+ {title} +
+
+ """ + + html += "
" + return html + +def generate_html_report(base_dir='dashboard_exports', output_file=None): + """ + Generate an HTML report with all dashboard widget images from multiple environments. + """ + base_path = Path(base_dir) + + if not base_path.exists(): + print(f"Error: Base directory {base_dir} not found") + return None # Default output filename if output_file is None: timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - output_file = f'{images_dir}/dashboard_report_{timestamp}.html' + output_file = f'{base_dir}/dashboard_report_{timestamp}.html' # Get dashboard name and timestamp from definition file dashboard_name = "Monthly Demand And Capacity Report - EliD" @@ -136,6 +168,32 @@ def generate_html_report(images_dir='dashboard_exports', output_file=None): padding-bottom: 48px; }} + .section-header {{ + background: var(--nhs-white); + padding: 16px 24px; + margin-bottom: 24px; + border-left: 8px solid var(--nhs-blue); + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + }} + + .section-header h2 {{ + font-size: 24px; + color: var(--nhs-blue); + margin: 0; + }} + + .env-section {{ + margin-bottom: 48px; + }} + + .env-title {{ + font-size: 20px; + color: var(--nhs-dark-grey); + margin-bottom: 20px; + padding-bottom: 8px; + border-bottom: 2px solid #d8dde0; + }} + .widget-card {{ background: var(--nhs-white); border: 1px solid #d8dde0; @@ -199,6 +257,11 @@ def generate_html_report(images_dir='dashboard_exports', output_file=None): border: none; border-bottom: 1px solid #ccc; }} + .section-header {{ + border: none; + padding: 0; + margin-bottom: 16px; + }} }} @@ -216,34 +279,28 @@ def generate_html_report(images_dir='dashboard_exports', output_file=None):
""" - # Add each widget image - for idx, image_file in enumerate(image_files, 1): - # Extract widget title from filename (remove number prefix and extension) - filename = image_file.stem - # Remove leading number and underscore (e.g., "01_") - title = filename.split('_', 1)[1] if '_' in filename else filename - # Replace underscores with spaces - title = title.replace('_', ' ') - - description = get_widget_description(title) - - # Read and encode image - with open(image_file, 'rb') as f: - image_data = base64.b64encode(f.read()).decode('utf-8') + # --------------------------------------------------------- + # Section 1: Production + # --------------------------------------------------------- + html_content += """ +
+

Production Environment

+
+ """ + html_content += generate_section_html("Prod", base_path / "Prod") - html_content += f""" -
-
-
{idx}. {title}
-
{description}
-
-
- {title} -
+ # --------------------------------------------------------- + # Section 2: Preprod Environments + # --------------------------------------------------------- + html_content += """ +
+

Preprod Environments

-""" + """ + + # Order: Preprod, Test, Dev + for env in ["Preprod", "Test", "Dev"]: + html_content += generate_section_html(env, base_path / env) # Close HTML html_content += """ From b708c04f60c19a0567d576679c32cfff7e1dddf6 Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:36:41 +0000 Subject: [PATCH 7/8] eli-420 trying a 'matrix' approach in Github Action --- .github/workflows/monthly-capacity-report.yml | 90 +++++++++---------- scripts/generate_dashboard_report.py | 1 - 2 files changed, 42 insertions(+), 49 deletions(-) diff --git a/.github/workflows/monthly-capacity-report.yml b/.github/workflows/monthly-capacity-report.yml index 7418444ba..ff140581f 100644 --- a/.github/workflows/monthly-capacity-report.yml +++ b/.github/workflows/monthly-capacity-report.yml @@ -13,80 +13,74 @@ permissions: id-token: write # Required for AWS OIDC authentication jobs: - generate-report: + export-dashboards: runs-on: ubuntu-latest environment: reporting + strategy: + matrix: + env_config: + - name: Prod + dashboard: Demand_And_Capacity_Prod + account_secret: AWS_PROD_ACCOUNT_ID + - name: Preprod + dashboard: Demand_And_Capacity_Preprod + account_secret: AWS_PREPROD_ACCOUNT_ID + - name: Test + dashboard: Demand_And_Capacity_Test + account_secret: AWS_TEST_ACCOUNT_ID + - name: Dev + dashboard: Demand_And_Capacity_Dev + account_secret: AWS_DEV_ACCOUNT_ID steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v5 with: python-version: "3.11" - # ---------------------------------------------------------------- - # 1. Export PROD - # ---------------------------------------------------------------- - - name: "Configure AWS Credentials (Prod)" + - name: Configure AWS Credentials (${{ matrix.env_config.name }}) uses: aws-actions/configure-aws-credentials@v5 with: - role-to-assume: arn:aws:iam::${{ secrets.AWS_PROD_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role + role-to-assume: arn:aws:iam::${{ secrets[matrix.env_config.account_secret] }}:role/service-roles/github-actions-api-deployment-role aws-region: eu-west-2 - - name: Export Dashboard (Prod) + - name: Export Dashboard (${{ matrix.env_config.name }}) run: | chmod +x scripts/export_dashboard_image.sh - ./scripts/export_dashboard_image.sh Demand_And_Capacity_Prod Prod + ./scripts/export_dashboard_image.sh ${{ matrix.env_config.dashboard }} ${{ matrix.env_config.name }} env: AWS_REGION: eu-west-2 - # ---------------------------------------------------------------- - # 2. Export PREPROD - # ---------------------------------------------------------------- - - name: "Configure AWS Credentials (Preprod)" - uses: aws-actions/configure-aws-credentials@v5 + - name: Upload dashboard export + uses: actions/upload-artifact@v4 with: - role-to-assume: arn:aws:iam::${{ secrets.AWS_PREPROD_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role - aws-region: eu-west-2 - - - name: Export Dashboard (Preprod) - run: ./scripts/export_dashboard_image.sh Demand_And_Capacity_Preprod Preprod - env: - AWS_REGION: eu-west-2 + name: dashboard-${{ matrix.env_config.name }} + path: dashboard_exports/**/* - # ---------------------------------------------------------------- - # 3. Export TEST - # ---------------------------------------------------------------- - - name: "Configure AWS Credentials (Test)" - uses: aws-actions/configure-aws-credentials@v5 - with: - role-to-assume: arn:aws:iam::${{ secrets.AWS_TEST_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role - aws-region: eu-west-2 + generate-report: + runs-on: ubuntu-latest + needs: export-dashboards + environment: reporting - - name: Export Dashboard (Test) - run: ./scripts/export_dashboard_image.sh Demand_And_Capacity_Test Test - env: - AWS_REGION: eu-west-2 + steps: + - name: Checkout code + uses: actions/checkout@v6 - # ---------------------------------------------------------------- - # 4. Export DEV - # ---------------------------------------------------------------- - - name: "Configure AWS Credentials (Dev)" - uses: aws-actions/configure-aws-credentials@v5 + - name: Set up Python + uses: actions/setup-python@v6 with: - role-to-assume: arn:aws:iam::${{ secrets.AWS_DEV_ACCOUNT_ID }}:role/service-roles/github-actions-api-deployment-role - aws-region: eu-west-2 + python-version: "3.11" - - name: Export Dashboard (Dev) - run: ./scripts/export_dashboard_image.sh Demand_And_Capacity_Dev Dev - env: - AWS_REGION: eu-west-2 + - name: Download all dashboard exports + uses: actions/download-artifact@v4 + with: + path: dashboard_exports + pattern: dashboard-* + merge-multiple: true - # ---------------------------------------------------------------- - # Generate & Notify - # ---------------------------------------------------------------- - name: Generate Combined Report run: python3 scripts/generate_dashboard_report.py --input dashboard_exports diff --git a/scripts/generate_dashboard_report.py b/scripts/generate_dashboard_report.py index af302f262..463b9ee0a 100644 --- a/scripts/generate_dashboard_report.py +++ b/scripts/generate_dashboard_report.py @@ -2,7 +2,6 @@ """ Generate an HTML report from exported dashboard images """ -import os import base64 from datetime import datetime from pathlib import Path From fc77123165f319780b0d94f64790d02084928cf9 Mon Sep 17 00:00:00 2001 From: Edd Almond <102675624+eddalmond1@users.noreply.github.com> Date: Mon, 22 Dec 2025 11:03:09 +0000 Subject: [PATCH 8/8] eli-420 split out CSS into it's own file --- scripts/dashboard_report.css | 158 ++++++++++++++++++ scripts/generate_dashboard_report.py | 236 ++++++++------------------- 2 files changed, 222 insertions(+), 172 deletions(-) create mode 100644 scripts/dashboard_report.css diff --git a/scripts/dashboard_report.css b/scripts/dashboard_report.css new file mode 100644 index 000000000..280a78f91 --- /dev/null +++ b/scripts/dashboard_report.css @@ -0,0 +1,158 @@ +/* NHS Dashboard Report Styles */ +:root { + --nhs-blue: #005EB8; + --nhs-white: #FFFFFF; + --nhs-black: #231f20; + --nhs-dark-grey: #425563; + --nhs-mid-grey: #768692; + --nhs-pale-grey: #E8EDEE; + --nhs-warm-yellow: #FFB81C; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Frutiger W01", Arial, sans-serif; + background: var(--nhs-pale-grey); + color: var(--nhs-black); + line-height: 1.5; +} + +.nhs-header { + background-color: var(--nhs-blue); + color: var(--nhs-white); + padding: 24px 0; + margin-bottom: 32px; +} + +.nhs-container { + max-width: 960px; + margin: 0 auto; + padding: 0 16px; +} + +.nhs-logo { + font-weight: 700; + font-size: 24px; + letter-spacing: -0.5px; + display: inline-block; + margin-right: 16px; + padding-right: 16px; + border-right: 1px solid rgba(255, 255, 255, 0.3); +} + +.report-title { + font-size: 24px; + font-weight: 600; + display: inline-block; +} + +.report-meta { + margin-top: 8px; + font-size: 14px; + opacity: 0.9; +} + +.content { + padding-bottom: 48px; +} + +.section-header { + background: var(--nhs-white); + padding: 16px 24px; + margin-bottom: 24px; + border-left: 8px solid var(--nhs-blue); + box-shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +.section-header h2 { + font-size: 24px; + color: var(--nhs-blue); + margin: 0; +} + +.env-section { + margin-bottom: 48px; +} + +.env-title { + font-size: 20px; + color: var(--nhs-dark-grey); + margin-bottom: 20px; + padding-bottom: 8px; + border-bottom: 2px solid #d8dde0; +} + +.widget-card { + background: var(--nhs-white); + border: 1px solid #d8dde0; + border-bottom: 4px solid var(--nhs-blue); + margin-bottom: 32px; + padding: 24px; + page-break-inside: avoid; +} + +.widget-header { + margin-bottom: 16px; + border-bottom: 1px solid var(--nhs-pale-grey); + padding-bottom: 16px; +} + +.widget-title { + font-size: 19px; + font-weight: 600; + color: var(--nhs-black); + margin-bottom: 8px; +} + +.widget-description { + font-size: 16px; + color: var(--nhs-dark-grey); + background: #f0f4f5; + padding: 12px; + border-left: 4px solid var(--nhs-mid-grey); +} + +.widget-image-container { + margin-top: 20px; + text-align: center; +} + +.widget-image { + max-width: 100%; + height: auto; + border: 1px solid var(--nhs-pale-grey); +} + +.footer { + text-align: center; + padding: 32px 0; + color: var(--nhs-mid-grey); + font-size: 14px; + border-top: 1px solid #d8dde0; + margin-top: 48px; +} + +@media print { + body { + background: white; + } + .nhs-header { + background: white; + color: black; + border-bottom: 2px solid var(--nhs-blue); + } + .widget-card { + border: none; + border-bottom: 1px solid #ccc; + } + .section-header { + border: none; + padding: 0; + margin-bottom: 16px; + } +} diff --git a/scripts/generate_dashboard_report.py b/scripts/generate_dashboard_report.py index 463b9ee0a..29d4906aa 100644 --- a/scripts/generate_dashboard_report.py +++ b/scripts/generate_dashboard_report.py @@ -85,7 +85,6 @@ def generate_html_report(base_dir='dashboard_exports', output_file=None): base_path = Path(base_dir) if not base_path.exists(): - print(f"Error: Base directory {base_dir} not found") return None # Default output filename @@ -93,175 +92,27 @@ def generate_html_report(base_dir='dashboard_exports', output_file=None): timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') output_file = f'{base_dir}/dashboard_report_{timestamp}.html' - # Get dashboard name and timestamp from definition file dashboard_name = "Monthly Demand And Capacity Report - EliD" report_date = datetime.now().strftime('%d %B %Y at %H:%M') # Build HTML - html_content = f""" + # Read CSS from file to embed in HTML + css_path = Path(__file__).parent / "dashboard_report.css" + try: + with open(css_path, "r", encoding="utf-8") as css_file: + css_content = css_file.read() + except Exception as e: + print(f"[ERROR] Could not read CSS file: {e}") + css_content = "" + + html_content = f''' NHS Dashboard Report - {dashboard_name} @@ -274,10 +125,61 @@ def generate_html_report(base_dir='dashboard_exports', output_file=None):
-
+''' + + # --------------------------------------------------------- + # Section 1: Production + # --------------------------------------------------------- + html_content += """ +
+

Production Environment

+
+ """ + html_content += generate_section_html("Prod", base_path / "Prod") + + # --------------------------------------------------------- + # Section 2: Preprod Environments + # --------------------------------------------------------- + html_content += """ +
+

Preprod Environments

+
+ """ + + # Order: Preprod, Test, Dev + for env in ["Preprod", "Test", "Dev"]: + html_content += generate_section_html(env, base_path / env) + + # Close HTML + html_content += """ +
+ + + + """ + # Print output path for debugging + print(f"[DEBUG] Output HTML file will be written to: {output_file}") + # Ensure output directory exists + output_dir = Path(output_file).parent + if not output_dir.exists(): + print(f"[DEBUG] Creating output directory: {output_dir}") + output_dir.mkdir(parents=True, exist_ok=True) + + # Write HTML file with error handling + try: + with open(output_file, 'w', encoding='utf-8') as f: + f.write(html_content) + print(f"[DEBUG] Successfully wrote report to {output_file}") + except Exception as e: + print(f"[ERROR] Failed to write report: {e}") + # --------------------------------------------------------- # Section 1: Production # --------------------------------------------------------- @@ -314,17 +216,7 @@ def generate_html_report(base_dir='dashboard_exports', output_file=None): """ - # Write HTML file - with open(output_file, 'w', encoding='utf-8') as f: - f.write(html_content) - - print(f"\nāœ“ Capacity and Demand Report generated: {output_file}") - print(f"\nTo view:") - print(f" - Open in browser: file://{Path(output_file).absolute()}") - print(f" - Or run: xdg-open {output_file}") - - return output_file - +# Only run this block if the script is executed directly if __name__ == "__main__": import argparse