1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

Compare commits

...

8 Commits

Author SHA1 Message Date
Ferdinand Mütsch
d778612242 fix: remove authentication requirement from diagnostics endpoint 2022-04-18 21:32:30 +02:00
Ferdinand Mütsch
ff7d595a86 chore: do not run expensive jobs initially but only scheduled 2022-04-18 21:16:27 +02:00
Ferdinand Mütsch
9d7688957f chore: explicit width and height for front page images [ci skip] 2022-04-18 19:28:30 +02:00
Ferdinand Mütsch
179042f81b refactor: use cross join instead of subquery for populating summary items (see #350) 2022-04-18 17:15:09 +02:00
Ferdinand Mütsch
e6441f124c chore: adapt tests 2022-04-18 16:14:58 +02:00
Ferdinand Mütsch
15c391d1d4 chore: downgrade postgres driver 2022-04-18 16:07:52 +02:00
Ferdinand Mütsch
91c765202c fix: prevent large difference between aggregated and recomputed summaries (resolve #354) 2022-04-18 16:06:32 +02:00
Ferdinand Mütsch
5276f68918 fix: double counting when using precise missing intervals 2022-04-18 15:18:01 +02:00
16 changed files with 794 additions and 796 deletions

View File

@ -72,4 +72,5 @@ mail:
client_id:
client_secret:
quick_start: false # whether to skip initial tasks on application startup, like summary generation
quick_start: false # whether to skip initial tasks on application startup, like summary generation
skip_migrations: false # whether to intentionally not run database migrations, only use for dev purposes

View File

@ -142,16 +142,17 @@ type SMTPMailConfig struct {
}
type Config struct {
Env string `default:"dev" env:"ENVIRONMENT"`
Version string `yaml:"-"`
QuickStart bool `yaml:"quick_start" env:"WAKAPI_QUICK_START"`
InstanceId string `yaml:"-"` // only temporary, changes between runs
App appConfig
Security securityConfig
Db dbConfig
Server serverConfig
Sentry sentryConfig
Mail mailConfig
Env string `default:"dev" env:"ENVIRONMENT"`
Version string `yaml:"-"`
QuickStart bool `yaml:"quick_start" env:"WAKAPI_QUICK_START"`
SkipMigrations bool `yaml:"skip_migrations" env:"WAKAPI_SKIP_MIGRATIONS"`
InstanceId string `yaml:"-"` // only temporary, changes between runs
App appConfig
Security securityConfig
Db dbConfig
Server serverConfig
Sentry sentryConfig
Mail mailConfig
}
func (c *Config) CreateCookie(name, value string) *http.Cookie {

File diff suppressed because it is too large Load Diff

2
go.sum
View File

@ -331,6 +331,8 @@ gorm.io/driver/mysql v1.3.3 h1:jXG9ANrwBc4+bMvBcSl8zCfPBaVoPyBEBshA8dA93X8=
gorm.io/driver/mysql v1.3.3/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U=
gorm.io/driver/postgres v1.3.2 h1:1URWk4lHWJkcudB+9bxOcNNt3uk5VfB8V2mzTPOqjRg=
gorm.io/driver/postgres v1.3.2/go.mod h1:y0vEuInFKJtijuSGu9e5bs5hzzSzPK+LancpKpvbRBw=
gorm.io/driver/postgres v1.3.3 h1:y6DU2kJgDNisxfAlmxRaQZOIy4ytnuYrpzpSFYnSfCY=
gorm.io/driver/postgres v1.3.3/go.mod h1:y0vEuInFKJtijuSGu9e5bs5hzzSzPK+LancpKpvbRBw=
gorm.io/driver/postgres v1.3.4 h1:evZ7plF+Bp+Lr1mO5NdPvd6M/N98XtwHixGB+y7fdEQ=
gorm.io/driver/postgres v1.3.4/go.mod h1:y0vEuInFKJtijuSGu9e5bs5hzzSzPK+LancpKpvbRBw=
gorm.io/driver/sqlite v1.3.1 h1:bwfE+zTEWklBYoEodIOIBwuWHpnx52Z9zJFW5F33WLk=

14
main.go
View File

@ -2,6 +2,7 @@ package main
import (
"embed"
"github.com/muety/wakapi/migrations"
"io/fs"
"log"
"net"
@ -16,7 +17,6 @@ import (
"github.com/emvi/logbuch"
"github.com/gorilla/handlers"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/migrations"
"github.com/muety/wakapi/repositories"
"github.com/muety/wakapi/routes/api"
"github.com/muety/wakapi/services/mail"
@ -138,7 +138,9 @@ func main() {
defer sqlDb.Close()
// Migrate database schema
migrations.Run(db, config)
if !config.SkipMigrations {
migrations.Run(db, config)
}
// Repositories
aliasRepository = repositories.NewAliasRepository(db)
@ -167,11 +169,9 @@ func main() {
miscService = services.NewMiscService(userService, summaryService, keyValueService)
// Schedule background tasks
if !config.QuickStart {
go aggregationService.Schedule()
go miscService.ScheduleCountTotalTime()
go reportService.Schedule()
}
go aggregationService.Schedule()
go miscService.ScheduleCountTotalTime()
go reportService.Schedule()
routes.Init()

View File

@ -1,9 +1,10 @@
package repositories
import (
datastructure "github.com/duke-git/lancet/v2/datastructure/set"
"github.com/duke-git/lancet/v2/slice"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"time"
)
@ -24,7 +25,7 @@ func (r *SummaryRepository) GetAll() ([]*models.Summary, error) {
return nil, err
}
if err := r.populateItems(summaries); err != nil {
if err := r.populateItems(summaries, []clause.Interface{}); err != nil {
return nil, err
}
@ -40,17 +41,26 @@ func (r *SummaryRepository) Insert(summary *models.Summary) error {
func (r *SummaryRepository) GetByUserWithin(user *models.User, from, to time.Time) ([]*models.Summary, error) {
var summaries []*models.Summary
if err := r.db.
Where(&models.Summary{UserID: user.ID}).
Where("from_time >= ?", from.Local()).
Where("to_time <= ?", to.Local()).
Order("from_time asc").
// branch summaries are currently not persisted, as only relevant in combination with project filter
Find(&summaries).Error; err != nil {
queryConditions := []clause.Interface{
clause.Where{Exprs: r.db.Statement.BuildCondition("user_id = ?", user.ID)},
clause.Where{Exprs: r.db.Statement.BuildCondition("from_time >= ?", from.Local())},
clause.Where{Exprs: r.db.Statement.BuildCondition("to_time <= ?", to.Local())},
}
q := r.db.Model(&models.Summary{}).
Order("from_time asc")
for _, c := range queryConditions {
q.Statement.AddClause(c)
}
// branch summaries are currently not persisted, as only relevant in combination with project filter
if err := q.Find(&summaries).Error; err != nil {
return nil, err
}
if err := r.populateItems(summaries); err != nil {
if err := r.populateItems(summaries, queryConditions); err != nil {
return nil, err
}
@ -77,28 +87,29 @@ func (r *SummaryRepository) DeleteByUser(userId string) error {
}
// inplace
func (r *SummaryRepository) populateItems(summaries []*models.Summary) error {
summaryMap := map[uint]*models.Summary{}
summaryIds := datastructure.NewSet[uint]()
for _, s := range summaries {
if s.NumHeartbeats == 0 {
continue
}
summaryMap[s.ID] = s
summaryIds.Add(s.ID)
}
func (r *SummaryRepository) populateItems(summaries []*models.Summary, conditions []clause.Interface) error {
var items []*models.SummaryItem
if err := r.db.
Model(&models.SummaryItem{}).
Where("summary_id in ?", summaryIds.Values()).
Find(&items).Error; err != nil {
summaryMap := slice.GroupWith[*models.Summary, uint](summaries, func(s *models.Summary) uint {
return s.ID
})
q := r.db.Model(&models.SummaryItem{}).
Select("summary_items.*").
Joins("cross join summaries").
Where("summary_items.summary_id = summaries.id").
Where("num_heartbeats > ?", 0)
for _, c := range conditions {
q.Statement.AddClause(c)
}
if err := q.Find(&items).Error; err != nil {
return err
}
for _, item := range items {
l := summaryMap[item.SummaryID].ItemsByType(item.Type)
l := summaryMap[item.SummaryID][0].ItemsByType(item.Type)
*l = append(*l, item)
}

View File

@ -6,7 +6,6 @@ import (
"github.com/gorilla/mux"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
@ -29,9 +28,6 @@ func NewDiagnosticsApiHandler(userService services.IUserService, diagnosticsServ
func (h *DiagnosticsApiHandler) RegisterRoutes(router *mux.Router) {
r := router.PathPrefix("/plugins/errors").Subrouter()
r.Use(
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
)
r.Path("").Methods(http.MethodPost).HandlerFunc(h.Post)
}

View File

@ -74,6 +74,7 @@ func GetBadgeParams(r *http.Request, requestedUser *models.User) (*models.KeyedI
default:
// non-entity-specific request, just a general, in-total query
permitEntity = true
filters = &models.Filters{}
}
if !permitEntity {

View File

@ -45,13 +45,8 @@ type AggregationJob struct {
// Schedule a job to (re-)generate summaries every day shortly after midnight
func (srv *AggregationService) Schedule() {
// Run once initially
if err := srv.Run(datastructure.NewSet[string]()); err != nil {
logbuch.Fatal("failed to run AggregationJob: %v", err)
}
s := gocron.NewScheduler(time.Local)
s.Every(1).Day().At(srv.config.App.AggregationTime).Do(srv.Run, datastructure.NewSet[string]())
s.Every(1).Day().At(srv.config.App.AggregationTime).WaitForSchedule().Do(srv.Run, datastructure.NewSet[string]())
s.StartBlocking()
}

View File

@ -1,6 +1,7 @@
package services
import (
"github.com/duke-git/lancet/v2/mathutil"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"time"
@ -59,13 +60,26 @@ func (srv *DurationService) Get(from, to time.Time, user *models.User, filters *
continue
}
dur := d1.Time.T().Sub(latest.Time.T().Add(latest.Duration))
if dur > HeartbeatDiffThreshold {
dur = HeartbeatDiffThreshold
sameDay := d1.Time.T().Day() == latest.Time.T().Day()
dur := time.Duration(mathutil.Min(
int64(d1.Time.T().Sub(latest.Time.T().Add(latest.Duration))),
int64(HeartbeatDiffThreshold),
))
// skip heartbeats that span across two adjacent summaries (assuming there are no more than 1 summary per day)
// this is relevant to prevent the time difference between generating summaries from raw heartbeats and aggregating pre-generated summaries
// for the latter case, the very last heartbeat of a day won't be counted, so we don't want to count it here either
// another option would be to adapt the Summarize() method to always append up to HeartbeatDiffThreshold seconds to a day's very last duration
if !sameDay {
dur = 0
}
latest.Duration += dur
if dur >= HeartbeatDiffThreshold || latest.GroupHash != d1.GroupHash {
// start new "group" if:
// (a) heartbeats were too far apart each other,
// (b) if they are of a different entity or,
// (c) if they span across two days
if dur >= HeartbeatDiffThreshold || latest.GroupHash != d1.GroupHash || !sameDay {
list := mapping[d1.GroupHash]
if d0 := list[len(list)-1]; d0 != d1 {
mapping[d1.GroupHash] = append(mapping[d1.GroupHash], d1)

View File

@ -38,13 +38,8 @@ type CountTotalTimeResult struct {
}
func (srv *MiscService) ScheduleCountTotalTime() {
// Run once initially
if err := srv.runCountTotalTime(); err != nil {
logbuch.Fatal("failed to run CountTotalTimeJob: %v", err)
}
s := gocron.NewScheduler(time.Local)
s.Every(1).Hour().Do(srv.runCountTotalTime)
s.Every(1).Hour().WaitForSchedule().Do(srv.runCountTotalTime)
s.StartBlocking()
}

View File

@ -117,6 +117,12 @@ func (srv *SummaryService) Retrieve(from, to time.Time, user *models.User, filte
missingIntervals := srv.getMissingIntervals(from, to, summaries, false)
for _, interval := range missingIntervals {
if s, err := srv.Summarize(interval.Start, interval.End, user, filters); err == nil {
if len(missingIntervals) > 2 && s.FromTime.T().Equal(s.ToTime.T()) {
// little hack here: GetAllWithin will query for >= from_date
// however, for "in-between" / intra-day missing intervals, we want strictly > from_date to prevent double-counting
// to not have to rewrite many interfaces, we skip these summaries here
continue
}
summaries = append(summaries, s)
} else {
return nil, err
@ -401,14 +407,14 @@ func (srv *SummaryService) getMissingIntervals(from, to time.Time, summaries []*
// we always want to jump to beginning of next day
// however, if left summary ends already at midnight, we would instead jump to beginning of second-next day -> go back again
if td1.Sub(t1) == 24*time.Hour {
if td1.AddDate(0, 0, 1).Equal(t1) {
td1 = td1.Add(-1 * time.Hour)
}
}
// one or more day missing in between?
if td1.Before(td2) {
intervals = append(intervals, &models.Interval{Start: summaries[i].ToTime.T(), End: summaries[i+1].FromTime.T()})
intervals = append(intervals, &models.Interval{Start: t1, End: t2})
}
}

View File

@ -318,7 +318,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve() {
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)
suite.DurationService.AssertNumberOfCalls(suite.T(), "Get", 2+1)
}
func (suite *SummaryServiceTestSuite) TestSummaryService_Retrieve_DuplicateSummaries() {

View File

@ -2097,7 +2097,7 @@
"",
"pm.test(\"Correct content\", function () {",
" const jsonData = pm.response.json();",
" pm.expect(jsonData.data.text).to.eql('0 hrs 8 mins');",
" pm.expect(jsonData.data.text).to.eql('0 hrs 4 mins');",
"});"
],
"type": "text/javascript"

View File

@ -58,7 +58,7 @@
</p>
<div class="flex justify-center my-8">
<img alt="App screenshot" src="assets/images/screenshot.webp">
<img alt="App screenshot" src="assets/images/screenshot.webp" width="800px" height="513px" loading="lazy">
</div>
<div class="flex flex-col items-center mt-10">
@ -81,11 +81,11 @@
</div>
<div class="flex justify-center space-x-2 mt-12">
<img alt="License badge"
<img alt="License badge" loading="lazy"
src="https://badges.fw-web.space/github/license/muety/wakapi?color=%232F855A&style=flat-square">
<img alt="Go version badge"
<img alt="Go version badge" loading="lazy"
src="https://badges.fw-web.space/github/go-mod/go-version/muety/wakapi?color=%232F855A&style=flat-square">
<img alt="Wakapi coding time badge"
<img alt="Wakapi coding time badge" loading="lazy"
src="https://badges.fw-web.space/endpoint?color=%232F855A&style=flat-square&label=wakapi&url=https://wakapi.dev/api/compat/shields/v1/n1try/interval:any/project:wakapi">
</div>
</div>

View File

@ -1,3 +1,3 @@
<a id="logo-container" class="text-2xl font-semibold text-white inline-block align-middle" href="">
<img src="assets/images/logo.svg" width="110px" alt="Logo">
<img src="assets/images/logo.svg" width="110px" height="42px" alt="Logo">
</a>