diff --git a/models/summary.go b/models/summary.go index 4fed09b..d9a1e0f 100644 --- a/models/summary.go +++ b/models/summary.go @@ -13,6 +13,8 @@ const ( SummaryMachine uint8 = 4 ) +const UnknownSummaryKey = "unknown" + type Summary struct { ID uint `json:"-" gorm:"primary_key"` UserID string `json:"user_id" gorm:"not null; index:idx_time_summary_user"` @@ -45,3 +47,55 @@ type SummaryViewModel struct { Success string ApiKey string } + +/* Augments the summary in a way that at least one item is present for every type. +If a summary has zero items for a given type, but one or more for any of the other types, +the total summary duration can be derived from those and inserted as a dummy-item with key "unknown" +for the missing type. +For instance, the machine type was introduced post hoc. Accordingly, no "machine"-information is present in +the data for old heartbeats and summaries. If a user has two years of data without machine information and +one day with such, a "machine"-chart plotted from that data will reference a way smaller absolute total amount +of time than the other ones. +To avoid having to modify persisted data retrospectively, i.e. inserting a dummy SummaryItem for the new type, +such is generated dynamically here, considering the "machine" for all old heartbeats "unknown". +*/ +func (s *Summary) FillUnknown() { + types := []uint8{SummaryProject, SummaryLanguage, SummaryEditor, SummaryOS, SummaryMachine} + missingTypes := make([]uint8, 0) + typeItems := map[uint8]*[]*SummaryItem{ + SummaryProject: &s.Projects, + SummaryLanguage: &s.Languages, + SummaryEditor: &s.Editors, + SummaryOS: &s.OperatingSystems, + SummaryMachine: &s.Machines, + } + var somePresentType uint8 + + for _, t := range types { + if len(*typeItems[t]) == 0 { + missingTypes = append(missingTypes, t) + } else { + somePresentType = t + } + } + + // can't proceed if entire summary is empty + if len(missingTypes) == len(types) { + return + } + + // calculate total duration from any of the present sets of items + var timeSum time.Duration + for _, item := range *typeItems[somePresentType] { + timeSum += item.Total + } + + // construct dummy item for all missing types + for _, t := range missingTypes { + *typeItems[t] = append(*typeItems[t], &SummaryItem{ + Type: t, + Key: UnknownSummaryKey, + Total: timeSum, + }) + } +} diff --git a/services/summary.go b/services/summary.go index a029f6e..574498f 100644 --- a/services/summary.go +++ b/services/summary.go @@ -103,6 +103,10 @@ func (srv *SummaryService) Construct(from, to time.Time, user *models.User, reco if len(existingSummaries) > 0 { realFrom = existingSummaries[0].FromTime realTo = existingSummaries[len(existingSummaries)-1].ToTime + + for _, summary := range existingSummaries { + summary.FillUnknown() + } } if len(heartbeats) > 0 { t1, t2 := time.Time(heartbeats[0].Time), time.Time(heartbeats[len(heartbeats)-1].Time) @@ -197,7 +201,7 @@ func (srv *SummaryService) aggregateBy(heartbeats []*models.Heartbeat, summaryTy } if key == "" { - key = "unknown" + key = models.UnknownSummaryKey } if aliasedKey, err := srv.AliasService.GetAliasOrDefault(user.ID, summaryType, key); err == nil { diff --git a/static/assets/app.js b/static/assets/app.js index e587b83..453d3ec 100644 --- a/static/assets/app.js +++ b/static/assets/app.js @@ -57,7 +57,7 @@ function draw() { .map(p => { return { label: p.key, - data: [parseInt(p.total)], + data: [parseInt(p.total) / 60], backgroundColor: getRandomColor(p.key) } }) @@ -68,6 +68,14 @@ function draw() { legend: { display: false }, + scales: { + xAxes: [{ + scaleLabel: { + display: true, + labelString: 'Minutes' + } + }] + }, maintainAspectRatio: false, onResize: onChartResize } diff --git a/version.txt b/version.txt index afa2b35..b9268da 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.8.0 \ No newline at end of file +1.8.1 \ No newline at end of file