All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
ORPHANED_LOAD_BALANCER recommendation type that flags Services of type LoadBalancer that have no ready backing endpoints. A new LoadBalancerCollector lists all Services and Endpoints from the Kubernetes API and reports LoadBalancer Services whose selector matches no pods — the provisioned cloud LoadBalancer keeps billing hourly while routing traffic to nothing. Recommendations carry the Service name (pod_name) and namespace and suggest deleting the Service. Projected cost savings prefer the real per-Service LoadBalancer cost reported by OpenCost (OpenCostCollector.collect_lb_costs() aggregates allocations by service and reads the loadBalancerCosts map, annualized from the observation window); when OpenCost is unreachable or has no cost data, they fall back to the new LOAD_BALANCER_COST_PER_MONTH config (Helm: config.recommendations.loadBalancerCostPerMonth, default $18.00/month). CO2e savings are not projected because energy estimation currently only covers CPU usage. Wired into the API, startup scan, CLI, demo data, and the frontend (new type badge on the recommendations page and dashboard). The Helm chart ClusterRole now also grants read access to endpoints; existing releases need helm upgrade to pick up the new RBAC rule.ORPHANED_PERSISTENT_VOLUME recommendation type that flags PersistentVolumes whose claim is gone. A new PVCollector lists all PVs and PVCs from the Kubernetes API and reports volumes that are in Released phase (PVC deleted but not reclaimed) or whose claimRef references a non-existent PVC. Recommendations carry the PV name (cluster scope) and suggest deleting the volume to release provisioned storage. Projected cost savings prefer the real per-volume storage cost reported by OpenCost (OpenCostCollector.collect_pv_costs() aggregates allocations by persistentvolume and annualizes the window cost); when OpenCost is unreachable or has no cost data, they fall back to the provisioned capacity using the new STORAGE_COST_PER_GIB_MONTH config (Helm: config.recommendations.storageCostPerGibMonth, default $0.10/GiB-month). CO2e savings are not projected because energy estimation currently only covers CPU usage. Wired into the API, startup scan, CLI, demo data, and the frontend (new type badge on the recommendations page and dashboard). The Helm chart ClusterRole now grants read access to persistentvolumes and persistentvolumeclaims; existing releases need helm upgrade to pick up the new RBAC rule.CombinedMetricsRepository.recompute_carbon_with_latest_intensities, with a single bulk UPDATE on PostgreSQL). Provisional provider values (e.g. Wattnet data younger than ~4h, valid=false) are corrected in place once consolidated — metrics that already had an intensity are updated too, not only those without one.BaseElectricityProvider abstraction. A WattnetCollector fetches 15-minute carbon footprint data for 52 European zones from the EU-funded Wattnet API (Bearer token auth with automatic refresh). Select it with ELECTRICITY_PROVIDER=wattnet plus WATTNET_EMAIL/WATTNET_PASSWORD (Helm: config.electricityProvider, secrets.wattnetEmail, secrets.wattnetPassword). Zones outside Europe gracefully fall back to the default intensity map. See docs/wattnet.md for full details and the water-footprint roadmap.wattnet service in GET /api/v1/health/services; Wattnet credentials can be updated at runtime from the Settings page and are persisted to the Kubernetes Secret (wattnet_email/wattnet_password in POST /api/v1/config/services).ELECTRICITY_PROVIDER=wattnet) now report as inactive and are excluded from the overall health status and from the frontend startup health popup, so users are no longer nagged to configure a provider they are not using.ELECTRICITY_MAPS_TOKEN is now emitted only when Electricity Maps is the active provider; the Wattnet credentials warning only fires when Wattnet is selected.aiohttp upgraded 3.14.1 → 3.14.3; structlog promoted to a runtime dependency (26.1.0); frontend transitive dependencies upgraded (@sveltejs/acorn-typescript, acorn, brace-expansion, js-yaml, nanoid, postcss, and others) to remediate Trivy-flagged vulnerabilities.TRUST_AUTH_PROXY config flag enables forwarding authenticated requests through an external auth proxy (Authentik, Caddy). When enabled, the API reads the authenticated user identity from the X-Forwarded-User / X-Auth-Request-User headers instead of performing its own authentication, removing the need to expose credentials directly to the proxy. Special thanks to @kahnwong for the detailed reproduction information that led to this fix.structlog: A new LOG_FORMAT config option (json or text, default text) controls the log output format. JSON mode emits machine-readable structured log lines suitable for log aggregation pipelines (e.g. Loki). Exposed in Helm values.yaml via config.logFormat.MetricsCompressor now runs VACUUM on the main metrics tables after each compaction cycle. A dedicated pruning pass removes stale hourly-savings-ledger records older than the configured retention window, keeping storage usage bounded on long-running deployments.GET /api/v1/metrics — The endpoint now uses SQL COUNT(*) + LIMIT/OFFSET instead of loading the full result set into Python. A new METRICS_LIST_MAX_RANGE_DAYS config (default 30, exposed in Helm values.yaml via config.metricsListMaxRangeDays) rejects requests wider than the limit with HTTP 400 to prevent accidental OOM on large clusters.read_combined_metrics_page — New method on CombinedMetricsRepository (base Python fallback + optimised PostgreSQL UNION ALL implementation) for DB-level paginated reads.read_latest_per_pod — New method returning the single most-recent metric snapshot for each (namespace, pod_name) pair. PostgreSQL implementation uses DISTINCT ON for efficiency; base class falls back to Python deduplication.aggregate_grouped_row_count — New method returning the number of distinct (group_key × time_bucket) rows a grouped-aggregate export would produce, computed entirely in SQL (COUNT DISTINCT). Implemented for PostgreSQL and SQLite.read_latest_per_pod — refresh_metrics_from_db now calls read_latest_per_pod instead of loading the full metrics history and deduplicating in Python, eliminating a major OOM source for large clusters.pip to uv — All CI workflows, the Dockerfile, and developer documentation now use uv for dependency management and virtual-environment creation. A uv.lock lockfile replaces the previous requirements*.txt files, ensuring fully reproducible builds across environments.python-dotenv with pydantic-settings — config.py is now driven by pydantic-settings BaseSettings, giving automatic environment-variable parsing with type coercion, validation, and cleaner secret handling. python-dotenv removed from dependencies./metrics route and its associated Svelte page have been removed. Dashboard panels reordered accordingly.upstream sent invalid header 50x errors seen behind reverse-proxy setups.other slice, preventing the chart legend from overflowing the panel.ruff version in CI to match the local configuration, eliminating lint-step failures caused by version skew.OOMKilled, exit code 137) in greenkube-api — GET /api/v1/metrics, GET /api/v1/report/summary?aggregate=true, and the Prometheus gauge refresh all previously loaded up to 1 M+ CombinedMetric objects into memory. All three paths are now SQL-backed and never materialise full row sets in Python (#239).report/summary?aggregate=true uses pure SQL — The aggregate summary path calls aggregate_grouped_row_count (SQL COUNT DISTINCT) instead of invoking aggregate_metrics() over loaded rows, making large date ranges safe regardless of dataset size.NodeCollector and PodCollector now explicitly close the async kubernetes_asyncio API client after each collection cycle, preventing handle leaks on long-running pods.package.json / package-lock.json updated to remediate Trivy-flagged vulnerabilities in transitive frontend dependencies.annual_co2e_savings_grams) and annual cost savings (annual_cost_savings_usd), extrapolated from the observation window. Values are surfaced in the API response, the frontend recommendations page, and CLI output.GET /api/v1/report now accepts a 1y (calendar year-to-date) window and arbitrary start/end timestamps. Reports can additionally be grouped by namespace via the group_by_namespace query parameter.RecommendationRealization records and kicks off a background refresh of the savings attribution so the greenkube_co2e_savings_attributed_grams_total and greenkube_cost_savings_attributed_dollars_total gauges stay consistent as metrics accumulate.GET /api/v1/recommendations/top — New API endpoint returning the highest-impact active recommendations ranked by projected annual savings. Returns TopRecommendation DTOs with rank, projected annual CO₂e savings, and projected annual cost savings.greenkube_top_recommendations gauge — New Prometheus gauge exposing ranked active recommendations.$node and $region template variables removed — Both variables have been dropped from the dashboard.scripts/build_grafana_dashboard.py, together with the constants no longer referenced by any remaining panel.package.json / package-lock.json updated to remediate Trivy-flagged vulnerabilities in transitive frontend dependencies.open, in_progress, resolved, dismissed, snoozed). New API endpoints allow updating status, bulk-dismissing, and snoozing recommendations. DB migrations 0006 (lifecycle columns) and 0007 (upsert null-fix) applied for both PostgreSQL and SQLite.CollectionOrchestrator, MetricAssembler, MetricsCompressor, Scheduler, recommender v2, factory, SummaryRepository (SQLite), TimeseriesCacheRepository (SQLite), a full recommendation lifecycle end-to-end suite, and additional node repository / recommendation repository unit tests.tests/integration/test_real_database_repositories.py runs the full repository layer against live SQLite and PostgreSQL instances. docker-compose.test.yml spins up a throwaway PostgreSQL container for CI. Documentation added in docs/testing.md.scripts/build_grafana_dashboard.py): Complete rebuild of the Grafana JSON dashboard generation script with full PromQL aggregation correctness, instant-query bargauges for Top 3 panels, and a reduce transformation to fix bar-scale inflation from historical data.src/greenkube/api/metrics_endpoint.py. ServiceMonitor updated to include the new scrape path. Grafana dashboard v2 built on these metrics with corrected PromQL and consistent namespace/cluster filters across all panels.SavingsAttributor service (src/greenkube/core/savings_attributor.py) prorates projected annual CO₂e and cost savings to the actual observation window. New SavingsLedger Pydantic model, abstract BaseSavingsRepository, and PostgreSQL/SQLite implementations. DB migrations 0008 applied for both engines. Two new Prometheus gauges: greenkube_co2e_savings_attributed_grams_total and greenkube_cost_savings_attributed_dollars_total.GreenOptic company with a fully pre-populated dataset — realistic node topology, multi-namespace workloads, historical metrics, and a backfilled savings ledger aligned with resolved demo recommendations. DB migration 0009 adds an is_active activity-status column to node records. New RecommendationRealization service (src/greenkube/core/recommendation_realization.py) links realised savings to specific recommendations in the ledger./report page rebuilt with a new reportOptions.js module for configurable report parameters and a new date_utils.py helper on the backend. Report layout and export logic simplified.pod_name and namespace are now allowed to be NULL in the DB schema for node-scope recommendations, fixing an integrity error on save.DEFAULT_EMBODIED_EMISSIONS_KG lowered from 350 kg to 100 kg to better match the average embodied footprint of a cloud VM vCPU slice, producing more accurate Scope 3 estimates when Boavizta returns no data.dev.sum(max by (cluster)(…)) for cluster-level scalars, sum by (namespace)(…) for namespace breakdowns, max by (namespace, pod)(…) for pod-level topk, max by (node)(…) for node metrics) to prevent value multiplication from multiple scrape instances.README.md streamlined to a project overview; detailed reference content moved to dedicated files — docs/api.md, docs/cli.md, docs/configuration.md, and docs/prometheus-grafana.md.PROMETHEUS_URL, OPENCOST_API_URL, ELECTRICITY_MAPS_TOKEN, BOAVIZTA_API_URL) are now patched into the GreenKube Kubernetes Secret immediately after being saved, so they survive pod restarts and helm upgrade --reuse-values without manual intervention. A namespaced Role/RoleBinding grants the service account get+patch access to exactly the GreenKube Secret (no cluster-wide secret access).favicon.ico) instead of the Svelte placeholder SVG. The SVG favicon reference has been removed from app.html and build/index.html; favicon.ico is served with the correct image/vnd.microsoft.icon MIME type.GET /api/v1/metrics/by-namespace: New lightweight endpoint returning CO2e, embodied emissions, energy, and cost aggregated by namespace over a time window. Queries both combined_metrics (raw) and combined_metrics_hourly (archived) tables via a single UNION ALL + GROUP BY — avoids loading full row sets into memory.GET /api/v1/metrics/top-pods: New lightweight endpoint returning the top-N pods by CO2e over a time window, also using the dual-table UNION ALL + GROUP BY pattern. Dashboard donut and top-pods charts now call these two endpoints instead of the expensive GET /metrics route, eliminating OOM restarts when browsing large time ranges.metrics_summary + metrics_timeseries_cache): Two new database tables (migrations 0004 and 0005 for PostgreSQL and SQLite) store pre-aggregated KPI scalars and time-series buckets for five fixed windows (24h, 7d, 30d, 1y, ytd). Tables are refreshed hourly by the background scheduler, eliminating full-table scans on every dashboard load and preventing OOM errors on large datasets.SummaryRefresher: New src/greenkube/core/summary_refresher.py service that computes cluster-wide and per-namespace KPI totals and time-series buckets, then upserts them into the two cache tables. Supports adaptive granularity per window (hourly / daily / weekly / monthly buckets).SummaryRepository and TimeseriesCacheRepository: New abstract base classes in storage/base_repository.py with PostgreSQL and SQLite implementations.GET /api/v1/metrics/dashboard-summary — cached KPI scalars, optionally filtered by namespace.GET /api/v1/metrics/dashboard-timeseries/{window_slug} — cached time-series buckets for 24h, 7d, 30d, 1y, or ytd.POST /api/v1/metrics/dashboard-summary/refresh — trigger an on-demand background refresh (HTTP 202 Accepted).MetricsSummaryRow and TimeseriesCachePoint Pydantic models: New DTOs in src/greenkube/models/metrics.py representing rows from the two cache tables.24h, daily for 7d/30d, weekly for 1y, monthly for ytd — resulting in consistently readable x-axes regardless of the selected range.EmbodiedEmissionsService now injects a fallback embodied-emissions profile using DEFAULT_EMBODIED_EMISSIONS_KG (default: 350 kg CO2e) instead of silently using 0 g, which was incorrect. The resulting CombinedMetric is flagged is_estimated=True with a descriptive estimation_reasons entry. Exposed as config.boavizta.defaultEmbodiedEmissionsKg in values.yaml and DEFAULT_EMBODIED_EMISSIONS_KG in configmap.yaml.EmbodiedEmissionsService.is_embodied_fallback(): New helper method returns True when a node’s cached profile was produced by the fallback rather than a real Boavizta response, enabling the metric assembler to set estimation flags accurately.kubernetes_asyncio): The in-cluster Secret patch now correctly uses kubernetes_asyncio (the async client that is actually installed) instead of the sync kubernetes package. load_incluster_config() is called without await (it reads files synchronously); failures are caught and logged without interrupting the API response.elasticsearch and elasticsearch-dsl packages moved to an optional extra (pip install greenkube[elasticsearch]). All imports are now lazy (loaded only when the ES storage backend is actually selected), removing heavy transitive dependencies and startup warnings for users on PostgreSQL or SQLite.GREENKUBE_SECRET_NAME is a resource name, not a secret value — suppressed in .trivyignore with justification. KSV-0113 (Role granting secret access) also documented as intentional for the UI persistence feature.nova): The scheduler’s carbon-intensity collection loop now falls back to the node’s geographic region when the provider-specific zone identifier is not a recognised Electricity Maps zone code. This restores carbon-intensity data collection on OVH, Infomaniak, and similar OpenStack-based clouds where the K8s node zone label is set to nova rather than a country/region code.CollectionOrchestrator no longer collects nodes internally. Node collection is now an explicit Phase 1 in DataProcessor.run() that runs alone before any concurrent collection, preventing shared Kubernetes API client races and the cascade of Electricity Maps API errors they caused.DEFAULT_ZONE spurious warning: The NodeZoneMapper no longer emits a warning when the zone was actually resolved correctly — the warning was incorrectly triggered even when a valid DEFAULT_ZONE was set.CollectionOrchestrator was averaging pod CPU usage per node across timestamps instead of summing, causing underestimated energy figures on nodes with multiple measured pods.DataProcessor.run() pipeline restructured into four explicit phases: Phase 1 (node discovery, sequential), Phase 2 (zone resolution), Phase 3 (parallel metrics + Boavizta), Phase 4 (carbon-intensity prefetch + assembly). This eliminates the previous race condition and removes the redundant second collect_instance_types() K8s call that used to happen at the end of the pipeline.CollectionOrchestrator simplified: NodeCollector dependency removed; node enrichment for Prometheus instance-type labels now uses the nodes_info dict passed in from Phase 1, avoiding any duplicate K8s API calls.node:20-alpine to node:22-alpine. Both the builder and final runtime stages now run apt-get upgrade at build time to patch known OS CVEs (libssl3, zlib1g, ncurses, libc). The final image user (greenkube, UID/GID 10001) is created with an explicit groupadd/useradd and /sbin/nologin shell.runAsNonRoot: true, runAsUser/Group: 10001, allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities.drop: [ALL], and seccompProfile.type: RuntimeDefault. /tmp directories served by emptyDir volumes (64 MiB each) to satisfy Python’s runtime tmp needs under a read-only root.runAsUser/Group: 70 (upstream requirement), readOnlyRootFilesystem: true, capabilities.drop: [ALL], seccompProfile.type: RuntimeDefault. /var/run/postgresql and /tmp mounted as emptyDir volumes. PostgreSQL upgraded from 17-alpine to 18-alpine for longer upstream lifecycle.POSTGRES_INITDB_ARGS set to --auth-host=scram-sha-256 --auth-local=scram-sha-256 — replaces the default md5 password hashing with the stronger SCRAM-SHA-256 protocol. Liveness and readiness probes added via pg_isready.secrets from the ClusterRole resource list, eliminating the critical RBAC over-permission (KSV-0041) that allowed the service account to read cluster-wide secrets.SecurityHeadersMiddleware (Starlette BaseHTTPMiddleware) added to the FastAPI app, injecting seven OWASP-recommended headers on every response: X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, Permissions-Policy, Cache-Control, and a strict Content-Security-Policy. CORS is now restricted to GET, POST, OPTIONS methods and Authorization/Content-Type headers (previously wildcard)..github/workflows/security.yml workflow running on every push/PR to main/dev and weekly (Monday 06:00 UTC) — five jobs: Trivy image scan for the GreenKube image (exit 1 on CRITICAL/HIGH), Trivy image scan for PostgreSQL (informational), Trivy IaC config scan for Dockerfile + Helm chart, Trivy filesystem scan for Python dependencies, and npm audit for the frontend. SARIF results uploaded to GitHub Security..trivyignore: Documents eight upstream-unfixable CVEs (gosu/Go-stdlib CVEs in the Alpine postgres image, one OpenSSL CMS CVE, and one zlib utility CVE) with justifications and a quarterly review date.secrets.existingSecret: New secrets.existingSecret value allows passing the name of a pre-created Kubernetes Secret instead of letting the chart manage one. When set, the chart skips Secret creation entirely and all secrets.* inline values are ignored — recommended for production to avoid storing credentials in values.yaml.SQLiteNodeRepository now implements a Slowly Changing Dimensions Type 2 pattern to deduplicate node records across collection cycles. A separate node_snapshots_scd table stores only rows where tracked columns (instance_type, vcpu, memory_gb, region, provider, zone) actually changed, avoiding write amplification on stable clusters. Migration 0003 creates this table and the associated indexes.scope column: recommendation_history table now includes a scope TEXT column (values: pod, namespace, node) to allow filtering recommendations by granularity. pod_name and namespace columns are nullable for node-scope and cluster-scope recommendations. Applied in migration 0003 for both PostgreSQL and SQLite.DB_POOL_MIN_SIZE (default: 2) and DB_POOL_MAX_SIZE (default: 10) environment variables control asyncpg’s connection pool bounds. Exposed as db.poolMinSize / db.poolMaxSize in helm-chart/values.yaml and propagated via configmap.yaml.DB_STATEMENT_TIMEOUT_MS environment variable (default: 30000 ms) sets a per-statement timeout on the PostgreSQL connection pool via server_settings. Exposed as db.statementTimeoutMs in helm-chart/values.yaml.combined_metrics(namespace, timestamp), namespace_cache(last_seen), and carbon_intensity_history(datetime) to accelerate the most frequent query patterns.helm-chart/Chart.yaml enriched with full Artifact Hub annotations — artifacthub.io/category, artifacthub.io/screenshots (6 screenshots), artifacthub.io/links, artifacthub.io/recommendations, artifacthub.io/changes, artifacthub.io/images (linux/amd64 + linux/arm64), artifacthub.io/maintainers, and artifacthub.io/readme (fixes “no README” on the listing page). Chart now includes keywords, home, sources, and maintainers fields for richer search indexing.artifacthub-repo.yml: Artifact Hub repository metadata file with repositoryID for Verified Publisher badge. Automatically copied to gh-pages by the release workflow alongside index.yaml.llms.txt (greenkube-website/public/): LLM/AI crawler guidance file following the llms.txt convention — enables AI assistants (Claude, ChatGPT, Perplexity) to understand GreenKube when crawling the website.assets/demo-report.png and assets/demo-settings.png added to README, Chart.yaml Artifact Hub screenshots, and llms.txt.scripts/pg_upgrade_17_to_18.sh: New maintenance script to upgrade an existing PostgreSQL 17 data directory to version 18 in-place using a Kubernetes Job and pg_upgrade --link, preserving all data with an automatic backup.aggregate_summary and aggregate_timeseries now correctly query both the raw combined_metrics table and the pre-aggregated hourly_metrics table, ensuring historical reports cover the full retention window without gaps at the boundary between live and archived data.METRICS_AGGREGATED_RETENTION_DAYS now defaults to -1 (infinite retention), preserving all historical data by default. This is the correct default for CSRD/ESRS E1 compliance, which requires multi-year reporting. Set an explicit positive integer to enforce a rolling window.init-pgrun-perms: Added readOnlyRootFilesystem: true to the PostgreSQL init container’s securityContext, resolving the HIGH misconfiguration finding.svelte, vite, rollup, picomatch, devalue, and @sveltejs/kit to their latest compatible versions, resolving all HIGH-severity advisories.table-format step (exit-code 1, visible in log) and a separate sarif step (exit-code 0, uploaded to GitHub Security tab). Added pull: true to the Docker build step so the base image layers are always pulled fresh from the registry, preventing stale GHA cache from hiding unfixed CVEs.artifacthub-repo.yml: Owner name and email corrected to match the actual GitHub account (Hugo Lelievre / hugo@greenkube.cloud).src/greenkube/storage/ package is split into three sub-packages — storage/postgres/, storage/sqlite/, and storage/elastic/ — each with its own __init__.py. All cross-package imports updated. Test suite reorganized to mirror the new structure with dedicated tests/core/, tests/grafana/, and tests/helm/ directories.pyproject.toml: Added 20 SEO keywords, 5 new PyPI classifiers, and 4 additional project URLs (Documentation, Changelog, Docker Hub, Repository).release.yml: Release workflow now copies artifacthub-repo.yml to gh-pages on every release so Artifact Hub always picks up the latest metadata.scripts/sync_version.py: update_helm_chart_yaml() now also keeps the artifacthub.io/images annotation in sync with the new version on each release.NodeCollector: _detect_cloud_provider now recognises Scaleway nodes via k8s.scaleway.com/* labels (primary signal set by the Scaleway Cloud Controller Manager on every Kapsule node) and falls back to node.spec.provider_id starting with scaleway:// for clusters where those labels may be absent. _extract_node_pool returns k8s.scaleway.com/nodepool-name (with nodepool-id as a fallback). Scaleway region mappings (fr-par, nl-ams, pl-waw → Electricity Maps zones) and PUE profile (1.37) were already present in the data layer and are now fully wired up.HealthCheckService (src/greenkube/core/health.py) that performs periodic connectivity checks against all data sources — Prometheus, OpenCost, Electricity Maps, Boavizta, and Kubernetes. Each probe reports status (healthy, degraded, unreachable, unconfigured), latency, resolved URL, and whether the service was auto-discovered or manually configured.GET /api/v1/health/services endpoint: Returns aggregated health status for all data sources with per-service details. Supports ?force=true to bypass the 30-second cache and trigger fresh probes.GET /api/v1/health/services/{service_name} endpoint: Returns health status for a single named service.POST /api/v1/config/services endpoint: Allows updating service URLs (Prometheus, OpenCost, Boavizta) and the Electricity Maps token at runtime from the frontend. Changes are session-scoped and do not persist across pod restarts.ServiceHealth, HealthCheckResponse, and ServiceConfigUpdate Pydantic models in src/greenkube/models/health.py.HealthBadge component: Reusable Svelte component (frontend/src/lib/components/HealthBadge.svelte) for color-coded service health indicators.HealthPopup component: Modal component (frontend/src/lib/components/HealthPopup.svelte) for first-connection service configuration.--no-color flag (and NO_COLOR env var support) to disable Rich formatting for clean pipeline logs. New --fail-on-recommendations flag on greenkube recommend to exit with code 1 when recommendations are found. New --fail-on-co2-threshold and --fail-on-cost-threshold flags on greenkube report to enforce carbon/cost policy gates in CI/CD pipelines.frontend/tests/) with 133 tests across 8 files covering all JS utility modules (formatters, API client, Svelte stores, ECharts option builders) and Svelte components (StatCard, Card, DataState, HealthBadge) using @testing-library/svelte. Added npm test, npm run test:watch, and npm run test:coverage scripts./report route in the SvelteKit SPA — a full-featured report builder that lets users configure time range (1 h → 1 y), namespace filter, aggregation (hourly/daily/weekly/monthly/yearly) and export format (CSV or JSON), preview totals before downloading, then trigger a direct browser download — no CLI or kubectl exec required.GET /api/v1/report/summary endpoint: Returns a preview of the report (row count, unique pods/namespaces, CO₂e, embodied CO₂e, energy, cost) for the current filter/aggregation parameters.GET /api/v1/report/export endpoint: Streams a downloadable file (CSV or JSON) with correct Content-Disposition headers. Supports the same namespace, last, aggregate, and granularity parameters as the CLI greenkube report command.ReportSummaryResponse schema: New Pydantic response model in api/schemas.py.Config.get_pue_for_provider() now falls back to the raw DEFAULT_PUE environment variable (default 1.3) when a node’s cloud provider is absent or not in DATACENTER_PUE_PROFILES, instead of incorrectly re-resolving through self.DEFAULT_PUE (which returns the configured CLOUD_PROVIDER’s profile — e.g. AWS=1.15 — even for unrelated unknown nodes). The estimation_reasons message now correctly reports 1.3 for unknown providers.CLOUD_PROVIDER default changed from aws to unknown: The env var and helm-chart/values.yaml previously defaulted to "aws", silently applying AWS’s PUE profile (1.15) on clusters where no cloud provider was configured. The default is now "unknown", which correctly triggers the DEFAULT_PUE fallback (1.3) and produces an explicit warning log instead of a silent wrong value.health.status === 'healthy' while the API returns "ok". Fixed to health.status === 'ok'.SustainabilityScorer class (src/greenkube/core/sustainability_score.py) computes a composite 0–100 score (100 = perfect cluster) across seven weighted dimensions:
grid_intensity × PUE; penalises both dirty grids and inefficient datacentres equallyeffective_intensity = grid_intensity × PUE so that a high-PUE datacenter (e.g. OVH=1.37) is penalised relative to a hyperscaler-efficient one (e.g. GCP=1.09) even on the same electrical grid. Invalid/missing PUE safely defaults to 1.0.SustainabilityResult Pydantic model: Carries overall_score and a dimension_scores dict for structured downstream consumption.greenkube_sustainability_score{cluster} — composite 0–100 scoregreenkube_sustainability_dimension_score{cluster, dimension} — per-dimension breakdowncluster, namespace, pod, node, and region labels, matching kube-state-metrics conventions and enabling seamless Grafana variable-based filtering.cluster and region drop-down template variables added to the pre-built Grafana dashboard for multi-cluster/multi-region environments.docs/sustainability-score.md — full description of the 7-dimension scoring model, formulas, reference thresholds, and PUE impact table.carbon_intensity dimension → carbon_efficiency: The scoring dimension was renamed and its formula extended to include PUE (effective_intensity = grid_intensity × PUE). The raw Prometheus gauges greenkube_carbon_intensity_score and greenkube_carbon_intensity_zone are kept unchanged for backward compatibility.CLUSTER_NAME now propagated to the metrics endpoint so the cluster label is always populated.ci-cd.yml workflow with three focused workflows: ci.yml (lint & test on all PRs/pushes), dev-build.yml (dev Docker images on dev branch), release.yml (production builds triggered by semver git tags)dev-<sha> and dev-latest; release images use the semver version and latestvX.Y.Z tag is pushed — no more mutable version tagspre-install-check CRD validation job now uses a dedicated ServiceAccount created via a pre-install hook, fixing the race condition where the job started before the main ServiceAccount existedpost-install-hook ready-check job now uses a dedicated ServiceAccount with its own hook lifecycle, preventing “serviceaccount not found” errors during fresh installs and upgradestopology.kubernetes.io/zone=nova (OpenStack default AZ name) is now ignored and the lookup falls through to the region label (GRA11, RBX8, …); numeric suffixes are stripped (GRA11 → GRA) before CSV lookup — all OVH data-centres now resolve to the correct Electricity Maps zonenode.k8s.ovh/type (current OVHcloud MKS generation) are now correctly identified as provider ovh; the previous check only matched the legacy k8s.ovh.net/ prefixcloud_region_electricity_maps_mapping.csv with uppercase trigrams (GRA, RBX, SBG, WAW, BHS, LIM, ERI, VIN, HIL, YYZ, SGP, SYD, YNM) and all new-API long-form region IDs (eu-west-par, eu-west-gra, eu-central-waw, ca-east-bhs, us-east-vin, ap-southeast-sgp, ap-southeast-syd, ap-south-mum, …)ServiceMonitor and NetworkPolicy are now disabled by default — fresh installs no longer fail on clusters without the Prometheus Operator (monitoring.coreos.com/v1 CRD)pre-install-check hook that validates the Prometheus Operator CRD is present before creating a ServiceMonitor, with a clear actionable error messagesum() to prevent duplicate series when multiple targets report the same metricpod_name=None) when saving to history — prevents integrity errors and irrelevant entriesgreenkube start hanging in Docker containers due to buffered stdout; invisible INFO logs now correctly flushed to the consolemonitoring section comments to distinguish GreenKube→Prometheus (automatic) from Prometheus→GreenKube (optional, for Grafana)/api/v1/metrics/summary and /api/v1/metrics/timeseries: aggregation now happens directly in the database (SQLite and PostgreSQL) instead of loading all rows into Python — typically 10–20× faster for large datasets and demo modedashboards/greenkube-grafana.json with KPIs, time-series, per-namespace breakdown, node utilization, grid intensity, and recommendations panels/prometheus/metrics endpoint: Comprehensive metric exposition (CO₂e, cost, energy, CPU, memory, network, disk, restarts, nodes, grid intensity, recommendations) with correct label relabelinggreenkube demo command generates 7 days of realistic sample data (22 pods, 5 namespaces) in a standalone SQLite instance — explore the dashboard without a live clusterCarbonIntensityRepository split: Dedicated repository implementations per backend (Postgres, SQLite, Elasticsearch) following the same pattern as other repositoriesCollectionOrchestrator, MetricAssembler, NodeZoneMapper, PrometheusResourceMapper, CostNormalizer, HistoricalRangeProcessor, EmbodiedEmissionsServiceCONTRIBUTING.md)docs/architecture.mdGREENKUBE_API_KEY), configurable CORS origins, rate limiting via slowapiGET /api/v1/metrics now supports offset and limit query parametersHEALTHCHECK instruction for standalone usagehelm test connectivity validation via test-connection.yamlpreStop lifecycle hook on the API containerparse_duration() utility used by both CLI and APIConfig.reload() for clean test isolation%-formatting throughout the codebaseRecommendation model uses typed scope field instead of sentinel pod_name="*"recommend command now uses the unified recommendation engine (all 9 types) instead of legacy 2-type APIrecommend reads from database by default (consistent with API); added --live flag for real-time moderead_combined_metrics_from_database() called with correct parameter names (start_time/end_time)run_range() now divides range total by number of time stepsUSER_AGENT header dynamically reflects the actual package versionDEFAULT_COST class attribute from ConfigrecommendSystemNamespaces moved inside recommendations scope in values.yamlcollect_detailed_info() now delegates to collect() to avoid inconsistent results.tgz artifacts from git tracking/api/v1/metrics/summary and /api/v1/metrics/timeseries: Aggregation is now performed directly in the database (SQLite and PostgreSQL) instead of loading all rows into Python objects — typically 10–20× faster for large datasets and demo mode/metrics)