mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
81835a3d88 | |||
30de96950b | |||
11291b0d6c | |||
f0ac0f6153 | |||
6aad1633e1 | |||
c07a4d71a0 |
19
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: Bug
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is. Please briefly describe how to reproduce the bug as well as _expected_ vs. _actual_ behavior. Optionally include screenshots and server logs, if helpful.
|
||||||
|
|
||||||
|
**System information**
|
||||||
|
Please provide information on:
|
||||||
|
* Wakapi version
|
||||||
|
* Operating system
|
||||||
|
* If Linux: which distro?
|
||||||
|
* If Docker: which image and tag?
|
||||||
|
* Database (SQLite, MySQL, ... ?)
|
10
.github/ISSUE_TEMPLATE/other.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/other.md
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
name: Other (feature request, question, ...)
|
||||||
|
about: Anything else
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ RUN cp /src/wakapi . && \
|
|||||||
FROM alpine:3
|
FROM alpine:3
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk update && apk add bash ca-certificates && rm -rf /var/cache/apk
|
RUN apk update && apk add bash ca-certificates tzdata && rm -rf /var/cache/apk
|
||||||
|
|
||||||
# See README.md and config.default.yml for all config options
|
# See README.md and config.default.yml for all config options
|
||||||
ENV ENVIRONMENT prod
|
ENV ENVIRONMENT prod
|
||||||
|
@ -22,6 +22,7 @@ type Heartbeat struct {
|
|||||||
Editor string `json:"editor" hash:"ignore"` // ignored because editor might be parsed differently by wakatime
|
Editor string `json:"editor" hash:"ignore"` // ignored because editor might be parsed differently by wakatime
|
||||||
OperatingSystem string `json:"operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
|
OperatingSystem string `json:"operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
|
||||||
Machine string `json:"machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
|
Machine string `json:"machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
|
||||||
|
UserAgent string `json:"user_agent" hash:"ignore"`
|
||||||
Time CustomTime `json:"time" gorm:"type:timestamp; index:idx_time,idx_time_user" swaggertype:"primitive,number"`
|
Time CustomTime `json:"time" gorm:"type:timestamp; index:idx_time,idx_time_user" swaggertype:"primitive,number"`
|
||||||
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"`
|
||||||
|
@ -160,10 +160,6 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.RowsAffected != 1 {
|
|
||||||
return nil, errors.New("nothing updated")
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +79,8 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
opSys, editor, _ := utils.ParseUserAgent(r.Header.Get("User-Agent"))
|
userAgent := r.Header.Get("User-Agent")
|
||||||
|
opSys, editor, _ := utils.ParseUserAgent(userAgent)
|
||||||
machineName := r.Header.Get("X-Machine-Name")
|
machineName := r.Header.Get("X-Machine-Name")
|
||||||
|
|
||||||
for _, hb := range heartbeats {
|
for _, hb := range heartbeats {
|
||||||
@ -88,6 +89,7 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
|||||||
hb.Machine = machineName
|
hb.Machine = machineName
|
||||||
hb.User = user
|
hb.User = user
|
||||||
hb.UserID = user.ID
|
hb.UserID = user.ID
|
||||||
|
hb.UserAgent = userAgent
|
||||||
|
|
||||||
if !hb.Valid() {
|
if !hb.Valid() {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
@ -106,6 +106,7 @@ func (w *WakatimeHeartbeatImporter) ImportAll(user *models.User) <-chan *models.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://wakatime.com/api/v1/users/current/heartbeats?date=2021-02-05
|
// https://wakatime.com/api/v1/users/current/heartbeats?date=2021-02-05
|
||||||
|
// https://pastr.de/p/b5p4od5s8w0pfntmwoi117jy
|
||||||
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}
|
||||||
|
|
||||||
@ -134,6 +135,7 @@ func (w *WakatimeHeartbeatImporter) fetchHeartbeats(day string) ([]*wakatime.Hea
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://wakatime.com/api/v1/users/current/all_time_since_today
|
// https://wakatime.com/api/v1/users/current/all_time_since_today
|
||||||
|
// https://pastr.de/p/w8xb4biv575pu32pox7jj2gr
|
||||||
func (w *WakatimeHeartbeatImporter) fetchRange() (time.Time, time.Time, error) {
|
func (w *WakatimeHeartbeatImporter) fetchRange() (time.Time, time.Time, error) {
|
||||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
@ -168,6 +170,7 @@ func (w *WakatimeHeartbeatImporter) fetchRange() (time.Time, time.Time, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://wakatime.com/api/v1/users/current/user_agents
|
// https://wakatime.com/api/v1/users/current/user_agents
|
||||||
|
// https://pastr.de/p/05k5do8q108k94lic4lfl3pc
|
||||||
func (w *WakatimeHeartbeatImporter) fetchUserAgents() (map[string]*wakatime.UserAgentEntry, error) {
|
func (w *WakatimeHeartbeatImporter) fetchUserAgents() (map[string]*wakatime.UserAgentEntry, error) {
|
||||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
@ -195,6 +198,7 @@ func (w *WakatimeHeartbeatImporter) fetchUserAgents() (map[string]*wakatime.User
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://wakatime.com/api/v1/users/current/machine_names
|
// https://wakatime.com/api/v1/users/current/machine_names
|
||||||
|
// https://pastr.de/p/v58cv0xrupp3zvyyv8o6973j
|
||||||
func (w *WakatimeHeartbeatImporter) fetchMachineNames() (map[string]*wakatime.MachineEntry, error) {
|
func (w *WakatimeHeartbeatImporter) fetchMachineNames() (map[string]*wakatime.MachineEntry, error) {
|
||||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
@ -261,6 +265,7 @@ func mapHeartbeat(
|
|||||||
Editor: ua.Editor,
|
Editor: ua.Editor,
|
||||||
OperatingSystem: ua.Os,
|
OperatingSystem: ua.Os,
|
||||||
Machine: ma.Value,
|
Machine: ma.Value,
|
||||||
|
UserAgent: ua.Value,
|
||||||
Time: entry.Time,
|
Time: entry.Time,
|
||||||
Origin: OriginWakatime,
|
Origin: OriginWakatime,
|
||||||
OriginId: entry.Id,
|
OriginId: entry.Id,
|
||||||
|
@ -34,10 +34,10 @@ Chart.defaults.global.defaultFontColor = "#E2E8F0"
|
|||||||
Chart.defaults.global.defaultColor = "#E2E8F0"
|
Chart.defaults.global.defaultColor = "#E2E8F0"
|
||||||
|
|
||||||
String.prototype.toHHMMSS = function () {
|
String.prototype.toHHMMSS = function () {
|
||||||
var sec_num = parseInt(this, 10)
|
const sec_num = parseInt(this, 10)
|
||||||
var hours = Math.floor(sec_num / 3600)
|
let hours = Math.floor(sec_num / 3600)
|
||||||
var minutes = Math.floor((sec_num - (hours * 3600)) / 60)
|
let minutes = Math.floor((sec_num - (hours * 3600)) / 60)
|
||||||
var seconds = sec_num - (hours * 3600) - (minutes * 60)
|
let seconds = sec_num - (hours * 3600) - (minutes * 60)
|
||||||
|
|
||||||
if (hours < 10) {
|
if (hours < 10) {
|
||||||
hours = '0' + hours
|
hours = '0' + hours
|
||||||
@ -48,14 +48,14 @@ String.prototype.toHHMMSS = function () {
|
|||||||
if (seconds < 10) {
|
if (seconds < 10) {
|
||||||
seconds = '0' + seconds
|
seconds = '0' + seconds
|
||||||
}
|
}
|
||||||
return hours + ':' + minutes + ':' + seconds
|
return `${hours}:${minutes}:${seconds}`
|
||||||
}
|
}
|
||||||
|
|
||||||
String.prototype.toHHMM = function () {
|
String.prototype.toHHMM = function () {
|
||||||
const sec_num = parseInt(this, 10)
|
const sec_num = parseInt(this, 10)
|
||||||
const hours = Math.floor(sec_num / 3600)
|
const hours = Math.floor(sec_num / 3600)
|
||||||
const minutes = Math.floor((sec_num - (hours * 3600)) / 60)
|
const minutes = Math.floor((sec_num - (hours * 3600)) / 60)
|
||||||
return hours + ':' + minutes
|
return `${hours}:${minutes}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function draw(subselection) {
|
function draw(subselection) {
|
||||||
@ -80,7 +80,7 @@ function draw(subselection) {
|
|||||||
.filter((c, i) => shouldUpdate(i))
|
.filter((c, i) => shouldUpdate(i))
|
||||||
.forEach(c => c.destroy())
|
.forEach(c => c.destroy())
|
||||||
|
|
||||||
let projectChart = !projectsCanvas.classList.contains('hidden') && shouldUpdate(0)
|
let projectChart = projectsCanvas && !projectsCanvas.classList.contains('hidden') && shouldUpdate(0)
|
||||||
? new Chart(projectsCanvas.getContext('2d'), {
|
? new Chart(projectsCanvas.getContext('2d'), {
|
||||||
type: 'horizontalBar',
|
type: 'horizontalBar',
|
||||||
data: {
|
data: {
|
||||||
@ -114,7 +114,10 @@ function draw(subselection) {
|
|||||||
xAxes: [{
|
xAxes: [{
|
||||||
scaleLabel: {
|
scaleLabel: {
|
||||||
display: true,
|
display: true,
|
||||||
labelString: 'Seconds'
|
labelString: 'Duration (hh:mm:ss)'
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
callback: (label) => label.toString().toHHMMSS()
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
@ -125,7 +128,7 @@ function draw(subselection) {
|
|||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
|
|
||||||
let osChart = !osCanvas.classList.contains('hidden') && shouldUpdate(1)
|
let osChart = osCanvas && !osCanvas.classList.contains('hidden') && shouldUpdate(1)
|
||||||
? new Chart(osCanvas.getContext('2d'), {
|
? new Chart(osCanvas.getContext('2d'), {
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
data: {
|
data: {
|
||||||
@ -158,7 +161,7 @@ function draw(subselection) {
|
|||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
|
|
||||||
let editorChart = !editorsCanvas.classList.contains('hidden') && shouldUpdate(2)
|
let editorChart = editorsCanvas && !editorsCanvas.classList.contains('hidden') && shouldUpdate(2)
|
||||||
? new Chart(editorsCanvas.getContext('2d'), {
|
? new Chart(editorsCanvas.getContext('2d'), {
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
data: {
|
data: {
|
||||||
@ -191,7 +194,7 @@ function draw(subselection) {
|
|||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
|
|
||||||
let languageChart = !languagesCanvas.classList.contains('hidden') && shouldUpdate(3)
|
let languageChart = languagesCanvas && !languagesCanvas.classList.contains('hidden') && shouldUpdate(3)
|
||||||
? new Chart(languagesCanvas.getContext('2d'), {
|
? new Chart(languagesCanvas.getContext('2d'), {
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
data: {
|
data: {
|
||||||
@ -224,7 +227,7 @@ function draw(subselection) {
|
|||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
|
|
||||||
let machineChart = !machinesCanvas.classList.contains('hidden') && shouldUpdate(4)
|
let machineChart = machinesCanvas && !machinesCanvas.classList.contains('hidden') && shouldUpdate(4)
|
||||||
? new Chart(machinesCanvas.getContext('2d'), {
|
? new Chart(machinesCanvas.getContext('2d'), {
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
data: {
|
data: {
|
||||||
@ -257,7 +260,7 @@ function draw(subselection) {
|
|||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
|
|
||||||
let labelChart = !labelsCanvas.classList.contains('hidden') && shouldUpdate(5)
|
let labelChart = labelsCanvas && !labelsCanvas.classList.contains('hidden') && shouldUpdate(5)
|
||||||
? new Chart(labelsCanvas.getContext('2d'), {
|
? new Chart(labelsCanvas.getContext('2d'), {
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
data: {
|
data: {
|
||||||
@ -305,9 +308,12 @@ function parseTopN() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function togglePlaceholders(mask) {
|
function togglePlaceholders(mask) {
|
||||||
const placeholderElements = containers.map(c => c.querySelector('.placeholder-container'))
|
const placeholderElements = containers.map(c => c ? c.querySelector('.placeholder-container'): null)
|
||||||
|
|
||||||
for (let i = 0; i < mask.length; i++) {
|
for (let i = 0; i < mask.length; i++) {
|
||||||
|
if (placeholderElements[i] === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!mask[i]) {
|
if (!mask[i]) {
|
||||||
canvases[i].classList.add('hidden')
|
canvases[i].classList.add('hidden')
|
||||||
placeholderElements[i].classList.remove('hidden')
|
placeholderElements[i].classList.remove('hidden')
|
||||||
|
@ -1 +1 @@
|
|||||||
1.29.4
|
1.29.5
|
||||||
|
@ -23,13 +23,13 @@
|
|||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<label class="inline-block text-sm mb-1 text-gray-500" for="username">Username</label>
|
<label class="inline-block text-sm mb-1 text-gray-500" for="username">Username</label>
|
||||||
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
||||||
type="text" id="username"
|
type="text" id="username" autocomplete="username"
|
||||||
name="username" placeholder="Enter your username" minlength="1" required autofocus>
|
name="username" placeholder="Enter your username" minlength="1" required autofocus>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<label class="inline-block text-sm mb-1 text-gray-500" for="password">Password</label>
|
<label class="inline-block text-sm mb-1 text-gray-500" for="password">Password</label>
|
||||||
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
||||||
type="password" id="password"
|
type="password" id="password" autocomplete="current-password"
|
||||||
name="password" placeholder="******" minlength="6" required>
|
name="password" placeholder="******" minlength="6" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
|
@ -232,12 +232,6 @@
|
|||||||
|
|
||||||
{{ template "foot.tpl.html" . }}
|
{{ template "foot.tpl.html" . }}
|
||||||
|
|
||||||
<script>
|
|
||||||
window.addEventListener('load', function() {
|
|
||||||
document.getElementById('api-key-instruction').innerHTML = document.getElementById('api-key-container').value
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const languageColors = {{ .LanguageColors | json }}
|
const languageColors = {{ .LanguageColors | json }}
|
||||||
const editorColors = {{ .EditorColors | json }}
|
const editorColors = {{ .EditorColors | json }}
|
||||||
@ -251,14 +245,18 @@
|
|||||||
wakapiData.machines = {{ .Machines | json }}
|
wakapiData.machines = {{ .Machines | json }}
|
||||||
wakapiData.labels = {{ .Labels | json }}
|
wakapiData.labels = {{ .Labels | json }}
|
||||||
|
|
||||||
document.getElementById("to-date-picker").onchange = function () {
|
if (document.getElementById('to-date-picker') !== null) {
|
||||||
var input = document.getElementById("from-date-picker");
|
document.getElementById("to-date-picker").onchange = function () {
|
||||||
input.setAttribute("max", this.value);
|
var input = document.getElementById("from-date-picker");
|
||||||
}
|
input.setAttribute("max", this.value);
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("from-date-picker").onchange = function () {
|
document.getElementById("from-date-picker").onchange = function () {
|
||||||
var input = document.getElementById("to-date-picker");
|
var input = document.getElementById("to-date-picker");
|
||||||
input.setAttribute("min", this.value);
|
input.setAttribute("min", this.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.getElementById('api-key-instruction').innerHTML = document.getElementById('api-key-container').value
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
Reference in New Issue
Block a user