mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
chore: optimize import date range
This commit is contained in:
@@ -68,7 +68,7 @@ I'd love to get some community feedback from active Wakapi users. If you want, p
|
|||||||
Plans for the near future mainly include, besides usual improvements and bug fixes, a UI redesign as well as additional types of charts and statistics (see [#101](https://github.com/muety/wakapi/issues/101), [#80](https://github.com/muety/wakapi/issues/80), [#76](https://github.com/muety/wakapi/issues/76), [#12](https://github.com/muety/wakapi/issues/12)). If you have feature requests or any kind of improvement proposals feel free to open an issue or share them in our [user survey](https://github.com/muety/wakapi/issues/82).
|
Plans for the near future mainly include, besides usual improvements and bug fixes, a UI redesign as well as additional types of charts and statistics (see [#101](https://github.com/muety/wakapi/issues/101), [#80](https://github.com/muety/wakapi/issues/80), [#76](https://github.com/muety/wakapi/issues/76), [#12](https://github.com/muety/wakapi/issues/12)). If you have feature requests or any kind of improvement proposals feel free to open an issue or share them in our [user survey](https://github.com/muety/wakapi/issues/82).
|
||||||
|
|
||||||
## ⌨️ How to use?
|
## ⌨️ How to use?
|
||||||
There are different options for how to use Wakapi, ranging from out hosted cloud service to self-hosting it. Regardless of which option choose, you will always have to do the [client setup](#-client-setup) in addition.
|
There are different options for how to use Wakapi, ranging from our hosted cloud service to self-hosting it. Regardless of which option choose, you will always have to do the [client setup](#-client-setup) in addition.
|
||||||
|
|
||||||
### ☁️ Option 1: Use [wakapi.dev](https://wakapi.dev)
|
### ☁️ Option 1: Use [wakapi.dev](https://wakapi.dev)
|
||||||
If you want to you out free, hosted cloud service, all you need to do is create an account and the set up your client-side tooling (see below).
|
If you want to you out free, hosted cloud service, all you need to do is create an account and the set up your client-side tooling (see below).
|
||||||
|
@@ -25,6 +25,7 @@ type Heartbeat struct {
|
|||||||
Time CustomTime `json:"time" gorm:"type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time,idx_time_user"`
|
Time CustomTime `json:"time" gorm:"type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time,idx_time_user"`
|
||||||
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
|
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
|
||||||
Origin string `json:"-" hash:"ignore"`
|
Origin string `json:"-" hash:"ignore"`
|
||||||
|
OriginId string `json:"-" hash:"ignore"`
|
||||||
languageRegex *regexp.Regexp `hash:"ignore"`
|
languageRegex *regexp.Regexp `hash:"ignore"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -37,6 +37,21 @@ func (r *HeartbeatRepository) CountByUser(user *models.User) (int64, error) {
|
|||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *HeartbeatRepository) GetLatestByOriginAndUser(origin string, user *models.User) (*models.Heartbeat, error) {
|
||||||
|
var heartbeat models.Heartbeat
|
||||||
|
if err := r.db.
|
||||||
|
Model(&models.Heartbeat{}).
|
||||||
|
Where(&models.Heartbeat{
|
||||||
|
UserID: user.ID,
|
||||||
|
Origin: origin,
|
||||||
|
}).
|
||||||
|
Order("time desc").
|
||||||
|
First(&heartbeat).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &heartbeat, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *HeartbeatRepository) GetAllWithin(from, to time.Time, user *models.User) ([]*models.Heartbeat, error) {
|
func (r *HeartbeatRepository) GetAllWithin(from, to time.Time, user *models.User) ([]*models.Heartbeat, error) {
|
||||||
var heartbeats []*models.Heartbeat
|
var heartbeats []*models.Heartbeat
|
||||||
if err := r.db.
|
if err := r.db.
|
||||||
|
@@ -20,6 +20,7 @@ type IHeartbeatRepository interface {
|
|||||||
CountByUser(*models.User) (int64, error)
|
CountByUser(*models.User) (int64, error)
|
||||||
GetAllWithin(time.Time, time.Time, *models.User) ([]*models.Heartbeat, error)
|
GetAllWithin(time.Time, time.Time, *models.User) ([]*models.Heartbeat, error)
|
||||||
GetFirstByUsers() ([]*models.TimeByUser, error)
|
GetFirstByUsers() ([]*models.TimeByUser, error)
|
||||||
|
GetLatestByOriginAndUser(string, *models.User) (*models.Heartbeat, error)
|
||||||
DeleteBefore(time.Time) error
|
DeleteBefore(time.Time) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -362,10 +362,18 @@ func (h *SettingsHandler) actionImportWaktime(w http.ResponseWriter, r *http.Req
|
|||||||
println(err)
|
println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var stream <-chan *models.Heartbeat
|
||||||
|
if latest, err := h.heartbeatSrvc.GetLatestByOriginAndUser(imports.OriginWakatime, user); latest == nil || err != nil {
|
||||||
|
stream = importer.ImportAll(user)
|
||||||
|
} else {
|
||||||
|
// if an import has happened before, only import heartbeats newer than the latest of the last import
|
||||||
|
stream = importer.Import(user, latest.Time.T(), time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
count := 0
|
count := 0
|
||||||
batch := make([]*models.Heartbeat, 0)
|
batch := make([]*models.Heartbeat, 0)
|
||||||
|
|
||||||
for hb := range importer.Import(user) {
|
for hb := range stream {
|
||||||
count++
|
count++
|
||||||
batch = append(batch, hb)
|
batch = append(batch, hb)
|
||||||
|
|
||||||
@@ -389,7 +397,7 @@ func (h *SettingsHandler) actionImportWaktime(w http.ResponseWriter, r *http.Req
|
|||||||
Value: time.Now().Format(time.RFC822),
|
Value: time.Now().Format(time.RFC822),
|
||||||
})
|
})
|
||||||
|
|
||||||
return http.StatusAccepted, "Import started. This may take a few minutes.", ""
|
return http.StatusAccepted, "ImportAll started. This may take a few minutes.", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SettingsHandler) actionRegenerateSummaries(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
func (h *SettingsHandler) actionRegenerateSummaries(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
||||||
|
@@ -42,6 +42,10 @@ func (srv *HeartbeatService) GetAllWithin(from, to time.Time, user *models.User)
|
|||||||
return srv.augmented(heartbeats, user.ID)
|
return srv.augmented(heartbeats, user.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (srv *HeartbeatService) GetLatestByOriginAndUser(origin string, user *models.User) (*models.Heartbeat, error) {
|
||||||
|
return srv.repository.GetLatestByOriginAndUser(origin, user)
|
||||||
|
}
|
||||||
|
|
||||||
func (srv *HeartbeatService) GetFirstByUsers() ([]*models.TimeByUser, error) {
|
func (srv *HeartbeatService) GetFirstByUsers() ([]*models.TimeByUser, error) {
|
||||||
return srv.repository.GetFirstByUsers()
|
return srv.repository.GetFirstByUsers()
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
package imports
|
package imports
|
||||||
|
|
||||||
import "github.com/muety/wakapi/models"
|
import (
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type HeartbeatImporter interface {
|
type HeartbeatImporter interface {
|
||||||
Import(*models.User) <-chan *models.Heartbeat
|
Import(*models.User, time.Time, time.Time) <-chan *models.Heartbeat
|
||||||
|
ImportAll(*models.User) <-chan *models.Heartbeat
|
||||||
}
|
}
|
||||||
|
@@ -17,9 +17,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const OriginWakatime = "wakatime"
|
||||||
maxWorkers = 6
|
const maxWorkers = 6
|
||||||
)
|
|
||||||
|
|
||||||
type WakatimeHeartbeatImporter struct {
|
type WakatimeHeartbeatImporter struct {
|
||||||
ApiKey string
|
ApiKey string
|
||||||
@@ -31,7 +30,7 @@ func NewWakatimeHeartbeatImporter(apiKey string) *WakatimeHeartbeatImporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WakatimeHeartbeatImporter) Import(user *models.User) <-chan *models.Heartbeat {
|
func (w *WakatimeHeartbeatImporter) Import(user *models.User, minFrom time.Time, maxTo time.Time) <-chan *models.Heartbeat {
|
||||||
out := make(chan *models.Heartbeat)
|
out := make(chan *models.Heartbeat)
|
||||||
|
|
||||||
go func(user *models.User, out chan *models.Heartbeat) {
|
go func(user *models.User, out chan *models.Heartbeat) {
|
||||||
@@ -41,6 +40,13 @@ func (w *WakatimeHeartbeatImporter) Import(user *models.User) <-chan *models.Hea
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if startDate.Before(minFrom) {
|
||||||
|
startDate = minFrom
|
||||||
|
}
|
||||||
|
if endDate.After(maxTo) {
|
||||||
|
endDate = maxTo
|
||||||
|
}
|
||||||
|
|
||||||
userAgents, err := w.fetchUserAgents()
|
userAgents, err := w.fetchUserAgents()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logbuch.Error("failed to fetch user agents while importing wakatime heartbeats for user '%s' – %v", user.ID, err)
|
logbuch.Error("failed to fetch user agents while importing wakatime heartbeats for user '%s' – %v", user.ID, err)
|
||||||
@@ -82,6 +88,10 @@ func (w *WakatimeHeartbeatImporter) Import(user *models.User) <-chan *models.Hea
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *WakatimeHeartbeatImporter) ImportAll(user *models.User) <-chan *models.Heartbeat {
|
||||||
|
return w.Import(user, time.Time{}, time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
// https://wakatime.com/api/v1/users/current/heartbeats?date=2021-02-05
|
// https://wakatime.com/api/v1/users/current/heartbeats?date=2021-02-05
|
||||||
func (w *WakatimeHeartbeatImporter) fetchHeartbeats(day string) ([]*wakatime.HeartbeatEntry, error) {
|
func (w *WakatimeHeartbeatImporter) fetchHeartbeats(day string) ([]*wakatime.HeartbeatEntry, error) {
|
||||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||||
@@ -211,7 +221,8 @@ func mapHeartbeat(
|
|||||||
OperatingSystem: ua.Os,
|
OperatingSystem: ua.Os,
|
||||||
Machine: entry.MachineNameId, // TODO
|
Machine: entry.MachineNameId, // TODO
|
||||||
Time: entry.Time,
|
Time: entry.Time,
|
||||||
Origin: fmt.Sprintf("wt@%s", entry.Id),
|
Origin: OriginWakatime,
|
||||||
|
OriginId: entry.Id,
|
||||||
}).Hashed()
|
}).Hashed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -31,6 +31,7 @@ type IHeartbeatService interface {
|
|||||||
CountByUser(*models.User) (int64, error)
|
CountByUser(*models.User) (int64, error)
|
||||||
GetAllWithin(time.Time, time.Time, *models.User) ([]*models.Heartbeat, error)
|
GetAllWithin(time.Time, time.Time, *models.User) ([]*models.Heartbeat, error)
|
||||||
GetFirstByUsers() ([]*models.TimeByUser, error)
|
GetFirstByUsers() ([]*models.TimeByUser, error)
|
||||||
|
GetLatestByOriginAndUser(string, *models.User) (*models.Heartbeat, error)
|
||||||
DeleteBefore(time.Time) error
|
DeleteBefore(time.Time) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -474,7 +474,9 @@
|
|||||||
const btnImportWakatime = document.querySelector('#btn-import-wakatime')
|
const btnImportWakatime = document.querySelector('#btn-import-wakatime')
|
||||||
const formImportWakatime = document.querySelector('#form-import-wakatime')
|
const formImportWakatime = document.querySelector('#form-import-wakatime')
|
||||||
btnImportWakatime.addEventListener('click', () => {
|
btnImportWakatime.addEventListener('click', () => {
|
||||||
|
if (confirm('Are you sure? The import can not be undone.')) {
|
||||||
formImportWakatime.submit()
|
formImportWakatime.submit()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user