From 92f6d44606799b8cb1f607761344aae03a3a1c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferdinand=20M=C3=BCtsch?= Date: Wed, 22 Dec 2021 10:17:05 +0100 Subject: [PATCH] feat: total heartbeats per summary (resolve #283) --- models/duration.go | 2 ++ models/durations.go | 8 ++++++++ models/summary.go | 1 + services/duration.go | 2 ++ services/duration_test.go | 4 ++++ services/summary.go | 8 ++++---- services/summary_test.go | 16 ++++++++++++++++ static/assets/vendor/twemoji.min.js | 2 -- views/summary.tpl.html | 6 +++++- 9 files changed, 42 insertions(+), 7 deletions(-) delete mode 100644 static/assets/vendor/twemoji.min.js diff --git a/models/duration.go b/models/duration.go index fbc3f8c..082dccf 100644 --- a/models/duration.go +++ b/models/duration.go @@ -16,6 +16,7 @@ type Duration struct { Editor string `json:"editor"` OperatingSystem string `json:"operating_system"` Machine string `json:"machine"` + NumHeartbeats int `json:"-" hash:"ignore"` GroupHash string `json:"-" hash:"ignore"` } @@ -29,6 +30,7 @@ func NewDurationFromHeartbeat(h *Heartbeat) *Duration { Editor: h.Editor, OperatingSystem: h.OperatingSystem, Machine: h.Machine, + NumHeartbeats: 1, } return d.Hashed() } diff --git a/models/durations.go b/models/durations.go index e541c86..e6b5b37 100644 --- a/models/durations.go +++ b/models/durations.go @@ -16,6 +16,14 @@ func (d Durations) Swap(i, j int) { d[i], d[j] = d[j], d[i] } +func (d Durations) TotalNumHeartbeats() int { + var total int + for _, e := range d { + total += e.NumHeartbeats + } + return total +} + func (d Durations) Sorted() Durations { sort.Sort(d) return d diff --git a/models/summary.go b/models/summary.go index 6d559bf..f7d4bb0 100644 --- a/models/summary.go +++ b/models/summary.go @@ -31,6 +31,7 @@ type Summary struct { OperatingSystems SummaryItems `json:"operating_systems" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Machines SummaryItems `json:"machines" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` Labels SummaryItems `json:"labels" gorm:"-"` // labels are not persisted, but calculated at runtime, i.e. when summary is retrieved + NumHeartbeats int `json:"-"` } type SummaryItems []*SummaryItem diff --git a/services/duration.go b/services/duration.go index f96150f..dc7f9d0 100644 --- a/services/duration.go +++ b/services/duration.go @@ -57,6 +57,8 @@ func (srv *DurationService) Get(from, to time.Time, user *models.User) (models.D mapping[d1.GroupHash] = append(mapping[d1.GroupHash], d1) } latest = d1 + } else { + latest.NumHeartbeats++ } count++ diff --git a/services/duration_test.go b/services/duration_test.go index f40111f..35f783c 100644 --- a/services/duration_test.go +++ b/services/duration_test.go @@ -140,6 +140,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get() { assert.Nil(suite.T(), err) assert.Len(suite.T(), durations, 1) assert.Equal(suite.T(), HeartbeatDiffThreshold, durations.First().Duration) + assert.Equal(suite.T(), 1, durations.First().NumHeartbeats) /* TEST 3 */ from, to = suite.TestStartTime, suite.TestStartTime.Add(1*time.Hour) @@ -155,6 +156,9 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get() { assert.Equal(suite.T(), TestEditorGoland, durations[0].Editor) assert.Equal(suite.T(), TestEditorGoland, durations[1].Editor) assert.Equal(suite.T(), TestEditorVscode, durations[2].Editor) + assert.Equal(suite.T(), 2, durations[0].NumHeartbeats) + assert.Equal(suite.T(), 1, durations[1].NumHeartbeats) + assert.Equal(suite.T(), 3, durations[2].NumHeartbeats) } func filterHeartbeats(from, to time.Time, heartbeats []*models.Heartbeat) []*models.Heartbeat { diff --git a/services/summary.go b/services/summary.go index 161084d..6e714d8 100644 --- a/services/summary.go +++ b/services/summary.go @@ -117,10 +117,8 @@ func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User) (*mod func (srv *SummaryService) Summarize(from, to time.Time, user *models.User) (*models.Summary, error) { // Initialize and fetch data - var durations models.Durations - if result, err := srv.durationService.Get(from, to, user); err == nil { - durations = result - } else { + durations, err := srv.durationService.Get(from, to, user) + if err != nil { return nil, err } @@ -169,6 +167,7 @@ func (srv *SummaryService) Summarize(from, to time.Time, user *models.User) (*mo Editors: editorItems, OperatingSystems: osItems, Machines: machineItems, + NumHeartbeats: durations.TotalNumHeartbeats(), } return summary.Sorted(), nil @@ -303,6 +302,7 @@ func (srv *SummaryService) mergeSummaries(summaries []*models.Summary) (*models. finalSummary.OperatingSystems = srv.mergeSummaryItems(finalSummary.OperatingSystems, s.OperatingSystems) finalSummary.Machines = srv.mergeSummaryItems(finalSummary.Machines, s.Machines) finalSummary.Labels = srv.mergeSummaryItems(finalSummary.Labels, s.Labels) + finalSummary.NumHeartbeats += s.NumHeartbeats processed[hash] = true } diff --git a/services/summary_test.go b/services/summary_test.go index 3691616..b7a4da4 100644 --- a/services/summary_test.go +++ b/services/summary_test.go @@ -44,6 +44,7 @@ func (suite *SummaryServiceTestSuite) SetupSuite() { Machine: TestMachine1, Time: models.CustomTime(suite.TestStartTime), Duration: 150 * time.Second, + NumHeartbeats: 2, }, { UserID: TestUserId, @@ -54,6 +55,7 @@ func (suite *SummaryServiceTestSuite) SetupSuite() { Machine: TestMachine1, Time: models.CustomTime(suite.TestStartTime.Add((30 + 130) * time.Second)), Duration: 20 * time.Second, + NumHeartbeats: 1, }, { UserID: TestUserId, @@ -64,6 +66,7 @@ func (suite *SummaryServiceTestSuite) SetupSuite() { Machine: TestMachine1, Time: models.CustomTime(suite.TestStartTime.Add(3 * time.Minute)), Duration: 15 * time.Second, + NumHeartbeats: 3, }, } suite.TestLabels = []*models.ProjectLabel{ @@ -114,6 +117,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() { assert.Equal(suite.T(), from, result.FromTime.T()) assert.Equal(suite.T(), to, result.ToTime.T()) assert.Zero(suite.T(), result.TotalTime()) + assert.Zero(suite.T(), result.NumHeartbeats) assert.Empty(suite.T(), result.Projects) /* TEST 2 */ @@ -127,6 +131,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() { assert.Equal(suite.T(), suite.TestDurations[0].Time.T(), result.FromTime.T()) assert.Equal(suite.T(), suite.TestDurations[0].Time.T(), result.ToTime.T()) assert.Equal(suite.T(), 150*time.Second, result.TotalTime()) + assert.Equal(suite.T(), 2, result.NumHeartbeats) assertNumAllItems(suite.T(), 1, result, "") /* TEST 3 */ @@ -142,6 +147,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Summarize() { assert.Equal(suite.T(), 185*time.Second, result.TotalTime()) assert.Equal(suite.T(), 170*time.Second, result.TotalTimeByKey(models.SummaryEditor, TestEditorGoland)) assert.Equal(suite.T(), 15*time.Second, result.TotalTimeByKey(models.SummaryEditor, TestEditorVscode)) + assert.Equal(suite.T(), 6, result.NumHeartbeats) assert.Len(suite.T(), result.Editors, 2) assertNumAllItems(suite.T(), 1, result, "e") } @@ -176,6 +182,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() { Editors: []*models.SummaryItem{}, OperatingSystems: []*models.SummaryItem{}, Machines: []*models.SummaryItem{}, + NumHeartbeats: 100, }, } @@ -189,6 +196,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() { assert.NotNil(suite.T(), result) assert.Len(suite.T(), result.Projects, 1) assert.Equal(suite.T(), summaries[0].Projects[0].Total*time.Second, result.TotalTime()) + assert.Equal(suite.T(), 100, result.NumHeartbeats) suite.DurationService.AssertNumberOfCalls(suite.T(), "Get", 2) /* TEST 2 */ @@ -210,6 +218,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() { Editors: []*models.SummaryItem{}, OperatingSystems: []*models.SummaryItem{}, Machines: []*models.SummaryItem{}, + NumHeartbeats: 100, }, { ID: uint(rand.Uint32()), @@ -227,6 +236,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() { Editors: []*models.SummaryItem{}, OperatingSystems: []*models.SummaryItem{}, Machines: []*models.SummaryItem{}, + NumHeartbeats: 100, }, } @@ -241,6 +251,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() { assert.Equal(suite.T(), 185*time.Second+90*time.Minute, result.TotalTime()) assert.Equal(suite.T(), 185*time.Second+45*time.Minute, result.TotalTimeByKey(models.SummaryProject, TestProject1)) assert.Equal(suite.T(), 45*time.Minute, result.TotalTimeByKey(models.SummaryProject, TestProject2)) + assert.Equal(suite.T(), 206, result.NumHeartbeats) suite.DurationService.AssertNumberOfCalls(suite.T(), "Get", 2+1) /* TEST 3 */ @@ -263,6 +274,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() { Editors: []*models.SummaryItem{}, OperatingSystems: []*models.SummaryItem{}, Machines: []*models.SummaryItem{}, + NumHeartbeats: 100, }, { ID: uint(rand.Uint32()), @@ -280,6 +292,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() { Editors: []*models.SummaryItem{}, OperatingSystems: []*models.SummaryItem{}, Machines: []*models.SummaryItem{}, + NumHeartbeats: 100, }, } @@ -294,6 +307,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() { assert.Equal(suite.T(), 90*time.Minute, result.TotalTime()) assert.Equal(suite.T(), 45*time.Minute, result.TotalTimeByKey(models.SummaryProject, TestProject1)) assert.Equal(suite.T(), 45*time.Minute, result.TotalTimeByKey(models.SummaryProject, TestProject2)) + assert.Equal(suite.T(), 200, result.NumHeartbeats) suite.DurationService.AssertNumberOfCalls(suite.T(), "Get", 2+1+1) } @@ -384,6 +398,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased() { assert.NotNil(suite.T(), result) assert.Zero(suite.T(), result.TotalTimeByKey(models.SummaryProject, TestProject1)) assert.NotZero(suite.T(), result.TotalTimeByKey(models.SummaryProject, TestProject2)) + assert.Equal(suite.T(), 6, result.NumHeartbeats) } func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased_ProjectLabels() { @@ -422,6 +437,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased_ProjectLabels() assert.Nil(suite.T(), err) assert.NotNil(suite.T(), result) assert.Equal(suite.T(), 195*time.Second, result.TotalTimeByKey(models.SummaryLabel, TestProjectLabel1)) + assert.Equal(suite.T(), 6, result.NumHeartbeats) } func filterDurations(from, to time.Time, durations models.Durations) models.Durations { diff --git a/static/assets/vendor/twemoji.min.js b/static/assets/vendor/twemoji.min.js deleted file mode 100644 index f34f4d3..0000000 --- a/static/assets/vendor/twemoji.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! Copyright Twitter Inc. and other contributors. Licensed under MIT */ -var twemoji=function(){"use strict";var twemoji={base:"https://twemoji.maxcdn.com/v/13.0.2/",ext:".png",size:"72x72",className:"emoji",convert:{fromCodePoint:fromCodePoint,toCodePoint:toCodePoint},onerror:function onerror(){if(this.parentNode){this.parentNode.replaceChild(createText(this.alt,false),this)}},parse:parse,replace:replace,test:test},escaper={"&":"&","<":"<",">":">","'":"'",'"':"""},re=/(?:\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d])|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d\udc08\u200d\u2b1b)|[#*0-9]\ufe0f?\u20e3|(?:[©®\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd0c\udd0f\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\udd77\uddb5\uddb6\uddb8\uddb9\uddbb\uddcd-\uddcf\uddd1-\udddd]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\uded5-\uded7\udeeb\udeec\udef4-\udefc\udfe0-\udfeb]|\ud83e[\udd0d\udd0e\udd10-\udd17\udd1d\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd3f-\udd45\udd47-\udd76\udd78\udd7a-\uddb4\uddb7\uddba\uddbc-\uddcb\uddd0\uddde-\uddff\ude70-\ude74\ude78-\ude7a\ude80-\ude86\ude90-\udea8\udeb0-\udeb6\udec0-\udec2\uded0-\uded6]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f/g,UFE0Fg=/\uFE0F/g,U200D=String.fromCharCode(8205),rescaper=/[&<>'"]/g,shouldntBeParsed=/^(?:iframe|noframes|noscript|script|select|style|textarea)$/,fromCharCode=String.fromCharCode;return twemoji;function createText(text,clean){return document.createTextNode(clean?text.replace(UFE0Fg,""):text)}function escapeHTML(s){return s.replace(rescaper,replacer)}function defaultImageSrcGenerator(icon,options){return"".concat(options.base,options.size,"/",icon,options.ext)}function grabAllTextNodes(node,allText){var childNodes=node.childNodes,length=childNodes.length,subnode,nodeType;while(length--){subnode=childNodes[length];nodeType=subnode.nodeType;if(nodeType===3){allText.push(subnode)}else if(nodeType===1&&!("ownerSVGElement"in subnode)&&!shouldntBeParsed.test(subnode.nodeName.toLowerCase())){grabAllTextNodes(subnode,allText)}}return allText}function grabTheRightIcon(rawText){return toCodePoint(rawText.indexOf(U200D)<0?rawText.replace(UFE0Fg,""):rawText)}function parseNode(node,options){var allText=grabAllTextNodes(node,[]),length=allText.length,attrib,attrname,modified,fragment,subnode,text,match,i,index,img,rawText,iconId,src;while(length--){modified=false;fragment=document.createDocumentFragment();subnode=allText[length];text=subnode.nodeValue;i=0;while(match=re.exec(text)){index=match.index;if(index!==i){fragment.appendChild(createText(text.slice(i,index),true))}rawText=match[0];iconId=grabTheRightIcon(rawText);i=index+rawText.length;src=options.callback(iconId,options);if(iconId&&src){img=new Image;img.onerror=options.onerror;img.setAttribute("draggable","false");attrib=options.attributes(rawText,iconId);for(attrname in attrib){if(attrib.hasOwnProperty(attrname)&&attrname.indexOf("on")!==0&&!img.hasAttribute(attrname)){img.setAttribute(attrname,attrib[attrname])}}img.className=options.className;img.alt=rawText;img.src=src;modified=true;fragment.appendChild(img)}if(!img)fragment.appendChild(createText(rawText,false));img=null}if(modified){if(i")}return ret})}function replacer(m){return escaper[m]}function returnNull(){return null}function toSizeSquaredAsset(value){return typeof value==="number"?value+"x"+value:value}function fromCodePoint(codepoint){var code=typeof codepoint==="string"?parseInt(codepoint,16):codepoint;if(code<65536){return fromCharCode(code)}code-=65536;return fromCharCode(55296+(code>>10),56320+(code&1023))}function parse(what,how){if(!how||typeof how==="function"){how={callback:how}}return(typeof what==="string"?parseString:parseNode)(what,{callback:how.callback||defaultImageSrcGenerator,attributes:typeof how.attributes==="function"?how.attributes:returnNull,base:typeof how.base==="string"?how.base:twemoji.base,ext:how.ext||twemoji.ext,size:how.folder||toSizeSquaredAsset(how.size||twemoji.size),className:how.className||twemoji.className,onerror:how.onerror||twemoji.onerror})}function replace(text,callback){return String(text).replace(re,callback)}function test(text){re.lastIndex=0;var result=re.test(text);re.lastIndex=0;return result}function toCodePoint(unicodeSurrogates,sep){var r=[],c=0,p=0,i=0;while(iTotal Time {{ .TotalTime | duration }} +
+ Total Heartbeats + {{ .NumHeartbeats }} +
Top Project {{ .MaxByToString 0 }} @@ -170,7 +174,7 @@
-
+
Labels