mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Compare commits
8 Commits
e774039831
...
d778612242
Author | SHA1 | Date | |
---|---|---|---|
|
d778612242 | ||
|
ff7d595a86 | ||
|
9d7688957f | ||
|
179042f81b | ||
|
e6441f124c | ||
|
15c391d1d4 | ||
|
91c765202c | ||
|
5276f68918 |
@ -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
|
@ -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
2
go.sum
@ -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
14
main.go
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user