From e85c47d05951286126f59ffce6108bd2d49b2c2b Mon Sep 17 00:00:00 2001 From: Vitalii Date: Tue, 13 Feb 2024 16:24:21 +0200 Subject: [PATCH] satellite/{console, emission, web}: calculate saved trees based on saved emission impact Added calculation of saved trees based on saved emission impact. Replaced Billing card with Savings card on prject dashboard. Issue: https://github.com/storj/storj/issues/6694 Change-Id: If55cb099881c3409ef55b93032058c9ea7c8514d --- .../console/consoleweb/consoleapi/projects.go | 10 +-- satellite/console/service.go | 25 +++++++- satellite/console/service_test.go | 13 ++-- satellite/emission/config.go | 1 + satellite/emission/service.go | 6 ++ satellite/satellite-config.yaml.lock | 3 + web/satellite/src/api/projects.ts | 2 +- web/satellite/src/types/projects.ts | 1 + web/satellite/src/views/Dashboard.vue | 62 +++++++++++++++---- 9 files changed, 91 insertions(+), 32 deletions(-) diff --git a/satellite/console/consoleweb/consoleapi/projects.go b/satellite/console/consoleweb/consoleapi/projects.go index 7c13eb8e05ad..fbb0bde4cd8b 100644 --- a/satellite/console/consoleweb/consoleapi/projects.go +++ b/satellite/console/consoleweb/consoleapi/projects.go @@ -494,15 +494,7 @@ func (p *Projects) GetEmissionImpact(w http.ResponseWriter, r *http.Request) { return } - var response struct { - StorjImpact float64 `json:"storjImpact"` - HyperscalerImpact float64 `json:"hyperscalerImpact"` - } - - response.StorjImpact = impact.EstimatedKgCO2eStorj - response.HyperscalerImpact = impact.EstimatedKgCO2eHyperscaler - - err = json.NewEncoder(w).Encode(response) + err = json.NewEncoder(w).Encode(impact) if err != nil { p.serveJSONError(ctx, w, http.StatusInternalServerError, err) } diff --git a/satellite/console/service.go b/satellite/console/service.go index a57d147c0e37..02cd382d4926 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -1728,8 +1728,16 @@ func (s *Service) GetSalt(ctx context.Context, projectID uuid.UUID) (salt []byte return s.store.Projects().GetSalt(ctx, isMember.project.ID) } +// EmissionImpactResponse represents emission impact response to be returned to client. +type EmissionImpactResponse struct { + StorjImpact float64 `json:"storjImpact"` + HyperscalerImpact float64 `json:"hyperscalerImpact"` + SavedTrees int64 `json:"savedTrees"` +} + // GetEmissionImpact is a method for querying project emission impact by id. -func (s *Service) GetEmissionImpact(ctx context.Context, projectID uuid.UUID) (impact *emission.Impact, err error) { +func (s *Service) GetEmissionImpact(ctx context.Context, projectID uuid.UUID) (*EmissionImpactResponse, error) { + var err error defer mon.Task()(&ctx)(&err) user, err := s.getUserAndAuditLog(ctx, "get project emission impact", zap.String("projectID", projectID.String())) if err != nil { @@ -1750,7 +1758,7 @@ func (s *Service) GetEmissionImpact(ctx context.Context, projectID uuid.UUID) (i period := now.Sub(isMember.project.CreatedAt) dataInTB := memory.Size(storageUsed).TB() - impact, err = s.emission.CalculateImpact(&emission.CalculationInput{ + impact, err := s.emission.CalculateImpact(&emission.CalculationInput{ AmountOfDataInTB: dataInTB, Duration: period, IsTBDuration: false, @@ -1759,7 +1767,18 @@ func (s *Service) GetEmissionImpact(ctx context.Context, projectID uuid.UUID) (i return nil, Error.Wrap(err) } - return impact, nil + savedValue := impact.EstimatedKgCO2eHyperscaler - impact.EstimatedKgCO2eStorj + if savedValue < 0 { + savedValue = 0 + } + + savedTrees := s.emission.CalculateSavedTrees(savedValue) + + return &EmissionImpactResponse{ + StorjImpact: impact.EstimatedKgCO2eStorj, + HyperscalerImpact: impact.EstimatedKgCO2eHyperscaler, + SavedTrees: savedTrees, + }, nil } // GetUsersProjects is a method for querying all projects. diff --git a/satellite/console/service_test.go b/satellite/console/service_test.go index 6d5a794aa4c0..387a01bdf373 100644 --- a/satellite/console/service_test.go +++ b/satellite/console/service_test.go @@ -37,7 +37,6 @@ import ( "storj.io/storj/satellite/buckets" "storj.io/storj/satellite/console" "storj.io/storj/satellite/console/consoleweb/consoleapi" - "storj.io/storj/satellite/emission" "storj.io/storj/satellite/nodeselection" "storj.io/storj/satellite/payments" "storj.io/storj/satellite/payments/billing" @@ -863,7 +862,7 @@ func TestService(t *testing.T) { impact, err := service.GetEmissionImpact(userCtx1, pr.ID) require.NoError(t, err) require.NotNil(t, impact) - require.EqualValues(t, emission.Impact{}, *impact) + require.EqualValues(t, console.EmissionImpactResponse{}, *impact) // Getting project salt as a non-member should not work impact, err = service.GetEmissionImpact(userCtx2, pr.ID) @@ -875,7 +874,7 @@ func TestService(t *testing.T) { now := time.Now().UTC() service.TestSetNow(func() time.Time { - return now.Add(24 * time.Hour) + return now.Add(365.25 * 24 * time.Hour) }) zeroValue := float64(0) @@ -883,11 +882,9 @@ func TestService(t *testing.T) { impact, err = service.GetEmissionImpact(userCtx1, pr.ID) require.NoError(t, err) require.NotNil(t, impact) - require.Greater(t, impact.EstimatedKgCO2eStorj, zeroValue) - require.Greater(t, impact.EstimatedKgCO2eHyperscaler, zeroValue) - require.Greater(t, impact.EstimatedKgCO2eCorporateDC, zeroValue) - require.Greater(t, impact.EstimatedFractionSavingsAgainstCorporateDC, zeroValue) - require.Greater(t, impact.EstimatedFractionSavingsAgainstHyperscaler, zeroValue) + require.Greater(t, impact.StorjImpact, zeroValue) + require.Greater(t, impact.HyperscalerImpact, zeroValue) + require.Greater(t, impact.SavedTrees, int64(0)) }) }) } diff --git a/satellite/emission/config.go b/satellite/emission/config.go index 4c132c707b70..46735b8ae710 100644 --- a/satellite/emission/config.go +++ b/satellite/emission/config.go @@ -29,4 +29,5 @@ type Config struct { HyperscalerUtilizationFraction float64 `help:"utilization fraction of hyperscaler networks, in fraction" default:"0.75"` CorporateDCUtilizationFraction float64 `help:"utilization fraction of corporate data center networks, in fraction" default:"0.40"` StorjUtilizationFraction float64 `help:"utilization fraction of storj network, in fraction" default:"0.85"` + AverageCO2SequesteredByTree float64 `help:"weighted average CO2 sequestered by a medium growth coniferous or deciduous tree, in kgCO2e/tree" default:"60"` } diff --git a/satellite/emission/service.go b/satellite/emission/service.go index e07429c0d2b6..d073e8404d2a 100644 --- a/satellite/emission/service.go +++ b/satellite/emission/service.go @@ -4,6 +4,7 @@ package emission import ( + "math" "time" "github.com/zeebo/errs" @@ -164,6 +165,11 @@ func (sv *Service) CalculateImpact(input *CalculationInput) (*Impact, error) { return rv, nil } +// CalculateSavedTrees calculates saved trees count based on emission impact. +func (sv *Service) CalculateSavedTrees(impact float64) int64 { + return int64(math.Round(impact / sv.config.AverageCO2SequesteredByTree)) +} + func (sv *Service) prepareExpansionFactorRow() *Row { storjExpansionFactor := unitless.Value(sv.config.StorjExpansionFactor) diff --git a/satellite/satellite-config.yaml.lock b/satellite/satellite-config.yaml.lock index 42e892221cf4..81db64628434 100644 --- a/satellite/satellite-config.yaml.lock +++ b/satellite/satellite-config.yaml.lock @@ -553,6 +553,9 @@ contact.external-address: "" # amount of time before sending second reminder to users who need to verify their email # email-reminders.second-verification-reminder: 120h0m0s +# weighted average CO2 sequestered by a medium growth coniferous or deciduous tree, in kgCO2e/tree +# emission.average-co2sequestered-by-tree: 60 + # carbon from power per year of operations, in kg/TB-year # emission.carbon-from-drive-powering: 15.9 diff --git a/web/satellite/src/api/projects.ts b/web/satellite/src/api/projects.ts index 3d0e65182895..42d91243c50e 100644 --- a/web/satellite/src/api/projects.ts +++ b/web/satellite/src/api/projects.ts @@ -277,7 +277,7 @@ export class ProjectsHttpApi implements ProjectsApi { } const json = await response.json(); - return json ? new Emission(json.storjImpact, json.hyperscalerImpact) : new Emission(); + return json ? new Emission(json.storjImpact, json.hyperscalerImpact, json.savedTrees) : new Emission(); } /** diff --git a/web/satellite/src/types/projects.ts b/web/satellite/src/types/projects.ts index 4bf88fbf05c2..f05b05ab4593 100644 --- a/web/satellite/src/types/projects.ts +++ b/web/satellite/src/types/projects.ts @@ -268,6 +268,7 @@ export class Emission { public constructor( public storjImpact: number = 0, public hyperscalerImpact: number = 0, + public savedTrees: number = 0, ) {} } diff --git a/web/satellite/src/views/Dashboard.vue b/web/satellite/src/views/Dashboard.vue index c3e5c2ea1f67..94cd7d98057d 100644 --- a/web/satellite/src/views/Dashboard.vue +++ b/web/satellite/src/views/Dashboard.vue @@ -50,17 +50,47 @@ - - - Estimated if using traditional cloud storage = {{ emission.hyperscalerImpact.toLocaleString(undefined, { maximumFractionDigits: 3 }) }} kg CO2e - - - - + + @@ -321,6 +351,16 @@ const isCreateBucketDialogOpen = ref(false); const isDatePicker = ref(false); const datePickerModel = ref([]); +/** + * Returns calculated and formatted CO2 savings info. + */ +const co2Savings = computed(() => { + let saved = emission.value.hyperscalerImpact - emission.value.storjImpact; + if (saved < 0) saved = 0; + + return `${saved.toLocaleString(undefined, { maximumFractionDigits: 3 })} kg CO2e`; +}); + /** * Returns formatted date range string. */