mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
56800be8e8 | |||
c149766ecc | |||
759e8e4dfd | |||
708863fd33 | |||
e2f046a83d | |||
30510591eb | |||
daf67b844a | |||
6b0b3bddda | |||
ef17d06763 | |||
301cab4be4 | |||
703805412b | |||
88eb68b1a9 | |||
8191a52ce1 | |||
5b3e88247e | |||
59b85863cc | |||
22fbfceca2 | |||
4d7fc6bff9 | |||
218c571859 | |||
e4c413a33c | |||
66b01c2797 | |||
0cee7496e0 |
38
README.md
38
README.md
@ -44,7 +44,6 @@
|
||||
* [Configuration Options](#-configuration-options)
|
||||
* [API Endpoints](#-api-endpoints)
|
||||
* [Integrations](#-integrations)
|
||||
* [WakaTime Integration](#%EF%B8%8F-wakatime-integration)
|
||||
* [Best Practices](#-best-practices)
|
||||
* [Developer Notes](#-developer-notes)
|
||||
* [Support](#-support)
|
||||
@ -62,6 +61,7 @@ I'd love to get some community feedback from active Wakapi users. If you want, p
|
||||
* ✅ Partially compatible with WakaTime
|
||||
* ✅ WakaTime integration
|
||||
* ✅ Support for Prometheus exports
|
||||
* ✅ Lightning fast
|
||||
* ✅ Self-hosted
|
||||
|
||||
## 🚧 Roadmap
|
||||
@ -164,7 +164,9 @@ You can specify configuration options either via a config file (default: `config
|
||||
| `server.base_path` | `WAKAPI_BASE_PATH` | `/` | Web base path (change when running behind a proxy under a sub-path) |
|
||||
| `security.password_salt` | `WAKAPI_PASSWORD_SALT` | - | Pepper to use for password hashing |
|
||||
| `security.insecure_cookies` | `WAKAPI_INSECURE_COOKIES` | `false` | Whether or not to allow cookies over HTTP |
|
||||
| `security.cookie_max_age` | `WAKAPI_COOKIE_MAX_AGE ` | `172800` | Lifetime of authentication cookies in seconds or `0` to use [Session](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Define_the_lifetime_of_a_cookie) cookies |
|
||||
| `security.cookie_max_age` | `WAKAPI_COOKIE_MAX_AGE` | `172800` | Lifetime of authentication cookies in seconds or `0` to use [Session](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Define_the_lifetime_of_a_cookie) cookies |
|
||||
| `security.allow_signup` | `WAKAPI_ALLOW_SIGNUP` | `true` | Whether to enable user registration |
|
||||
| `security.expose_metrics` | `WAKAPI_EXPOSE_METRICS` | `false` | Whether to expose Prometheus metrics under `/api/metrics` |
|
||||
| `db.host` | `WAKAPI_DB_HOST` | - | Database host |
|
||||
| `db.port` | `WAKAPI_DB_PORT` | - | Database port |
|
||||
| `db.user` | `WAKAPI_DB_USER` | - | Database user |
|
||||
@ -196,13 +198,37 @@ $ swag init -o static/docs
|
||||
|
||||
## 🤝 Integrations
|
||||
### Prometheus Export
|
||||
If you want to export your Wakapi statistics to Prometheus to view them in a Grafana dashboard or so please refer to an excellent tool called **[wakatime_exporter](https://github.com/MacroPower/wakatime_exporter)**.
|
||||
You can export your Wakapi statistics to Prometheus to view them in a Grafana dashboard or so. Here is how.
|
||||
|
||||
[](https://github.com/MacroPower/wakatime_exporter)
|
||||
```bash
|
||||
# 1. Start Wakapi with the feature enabled
|
||||
$ export WAKAPI_EXPOSE_METRICS=true
|
||||
$ ./wakapi
|
||||
|
||||
It is a standalone webserver that connects to your Wakapi instance and exposes the data as Prometheus metrics. Although originally developed to scrape data from WakaTime, it will mostly for with Wakapi as well, as the APIs are partially compatible.
|
||||
# 2. Get your API key and hash it
|
||||
$ echo "<YOUR_API_KEY>" | base64
|
||||
|
||||
Simply configure the exporter with `WAKA_SCRAPE_URI` to equal `"https://wakapi.your-server.com/api/compat/wakatime/v1"` and set your API key accordingly.
|
||||
# 3. Add a Prometheus scrape config to your prometheus.yml (see below)
|
||||
```
|
||||
|
||||
#### Scrape config example
|
||||
```yml
|
||||
# prometheus.yml
|
||||
# (assuming your Wakapi instance listens at localhost, port 3000)
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'wakapi'
|
||||
scrape_interval: 1m
|
||||
metrics_path: '/api/metrics'
|
||||
bearer_token: '<YOUR_BASE64_HASHED_TOKEN>'
|
||||
static_configs:
|
||||
- targets: ['localhost:3000']
|
||||
```
|
||||
|
||||
#### Grafana
|
||||
There is also a [nice Grafana dashboard](https://grafana.com/grafana/dashboards/12790), provided by the author of [wakatime_exporter](https://github.com/MacroPower/wakatime_exporter).
|
||||
|
||||

|
||||
|
||||
### WakaTime Integration
|
||||
Wakapi plays well together with [WakaTime](https://wakatime.com). For one thing, you can **forward heartbeats** from Wakapi to WakaTime to effectively use both services simultaneously. In addition, there is the option to **import historic data** from WakaTime for consistency between both services. Both features can be enabled in the _Integrations_ section of your Wakapi instance's settings page.
|
||||
|
@ -10,7 +10,7 @@ server:
|
||||
|
||||
app:
|
||||
aggregation_time: '02:15' # time at which to run daily aggregation batch jobs
|
||||
counting_time: '05:15' # time at which to run daily job to count total hours tracked in the system
|
||||
inactive_days: 7 # time of previous days within a user must have logged in to be considered active
|
||||
custom_languages:
|
||||
vue: Vue
|
||||
jsx: JSX
|
||||
@ -27,6 +27,7 @@ db:
|
||||
|
||||
security:
|
||||
password_salt: # CHANGE !
|
||||
insecure_cookies: false
|
||||
insecure_cookies: false # You need to set this to 'true' when on localhost
|
||||
cookie_max_age: 172800
|
||||
allow_signup: true
|
||||
allow_signup: true
|
||||
expose_metrics: false
|
@ -30,6 +30,12 @@ const (
|
||||
KeyLatestTotalTime = "latest_total_time"
|
||||
KeyLatestTotalUsers = "latest_total_users"
|
||||
KeyLastImportImport = "last_import"
|
||||
|
||||
SimpleDateFormat = "2006-01-02"
|
||||
SimpleDateTimeFormat = "2006-01-02 15:04:05"
|
||||
|
||||
ErrUnauthorized = "401 unauthorized"
|
||||
ErrInternalServerError = "500 internal server error"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -39,6 +45,7 @@ const (
|
||||
WakatimeApiHeartbeatsUrl = "/users/current/heartbeats"
|
||||
WakatimeApiHeartbeatsBulkUrl = "/users/current/heartbeats.bulk"
|
||||
WakatimeApiUserAgentsUrl = "/users/current/user_agents"
|
||||
WakatimeApiMachineNamesUrl = "/users/current/machine_names"
|
||||
)
|
||||
|
||||
var cfg *Config
|
||||
@ -46,15 +53,16 @@ var cFlag = flag.String("config", defaultConfigPath, "config file location")
|
||||
|
||||
type appConfig struct {
|
||||
AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"`
|
||||
CountingTime string `yaml:"counting_time" default:"05:15" env:"WAKAPI_COUNTING_TIME"`
|
||||
ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"`
|
||||
ImportBatchSize int `yaml:"import_batch_size" default:"100" env:"WAKAPI_IMPORT_BATCH_SIZE"`
|
||||
InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"`
|
||||
CustomLanguages map[string]string `yaml:"custom_languages"`
|
||||
Colors map[string]map[string]string `yaml:"-"`
|
||||
}
|
||||
|
||||
type securityConfig struct {
|
||||
AllowSignup bool `yaml:"allow_signup" default:"true" env:"WAKAPI_ALLOW_SIGNUP"`
|
||||
AllowSignup bool `yaml:"allow_signup" default:"true" env:"WAKAPI_ALLOW_SIGNUP"`
|
||||
ExposeMetrics bool `yaml:"expose_metrics" default:"false" env:"WAKAPI_EXPOSE_METRICS"`
|
||||
// this is actually a pepper (https://en.wikipedia.org/wiki/Pepper_(cryptography))
|
||||
PasswordSalt string `yaml:"password_salt" default:"" env:"WAKAPI_PASSWORD_SALT"`
|
||||
InsecureCookies bool `yaml:"insecure_cookies" default:"false" env:"WAKAPI_INSECURE_COOKIES"`
|
||||
|
@ -1,9 +1,4 @@
|
||||
mode: set
|
||||
github.com/muety/wakapi/models/alias.go:12.32,14.2 1 0
|
||||
github.com/muety/wakapi/models/alias.go:16.37,17.35 1 0
|
||||
github.com/muety/wakapi/models/alias.go:22.2,22.14 1 0
|
||||
github.com/muety/wakapi/models/alias.go:17.35,18.18 1 0
|
||||
github.com/muety/wakapi/models/alias.go:18.18,20.4 1 0
|
||||
github.com/muety/wakapi/models/heartbeat.go:32.34,34.2 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:36.65,37.28 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:40.2,41.45 2 1
|
||||
@ -38,71 +33,65 @@ github.com/muety/wakapi/models/heartbeats.go:34.18,36.3 1 0
|
||||
github.com/muety/wakapi/models/language_mapping.go:11.42,13.2 1 0
|
||||
github.com/muety/wakapi/models/language_mapping.go:15.51,17.2 1 0
|
||||
github.com/muety/wakapi/models/language_mapping.go:19.52,21.2 1 0
|
||||
github.com/muety/wakapi/models/models.go:3.14,5.2 0 1
|
||||
github.com/muety/wakapi/models/shared.go:34.52,37.16 3 0
|
||||
github.com/muety/wakapi/models/shared.go:40.2,42.12 3 0
|
||||
github.com/muety/wakapi/models/shared.go:37.16,39.3 1 0
|
||||
github.com/muety/wakapi/models/shared.go:46.52,52.22 2 0
|
||||
github.com/muety/wakapi/models/shared.go:68.2,71.12 3 0
|
||||
github.com/muety/wakapi/models/shared.go:53.14,55.17 2 0
|
||||
github.com/muety/wakapi/models/shared.go:58.13,60.8 2 0
|
||||
github.com/muety/wakapi/models/shared.go:61.17,63.8 2 0
|
||||
github.com/muety/wakapi/models/shared.go:64.10,65.64 1 0
|
||||
github.com/muety/wakapi/models/shared.go:55.17,57.4 1 0
|
||||
github.com/muety/wakapi/models/shared.go:74.45,76.2 1 0
|
||||
github.com/muety/wakapi/models/shared.go:78.51,81.2 2 0
|
||||
github.com/muety/wakapi/models/shared.go:83.37,86.2 2 0
|
||||
github.com/muety/wakapi/models/shared.go:88.35,90.2 1 0
|
||||
github.com/muety/wakapi/models/shared.go:92.34,94.2 1 0
|
||||
github.com/muety/wakapi/models/summary.go:67.29,69.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:71.37,78.2 6 1
|
||||
github.com/muety/wakapi/models/summary.go:80.35,82.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:84.57,92.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:105.33,110.26 4 1
|
||||
github.com/muety/wakapi/models/summary.go:117.2,117.37 1 1
|
||||
github.com/muety/wakapi/models/summary.go:121.2,124.33 2 1
|
||||
github.com/muety/wakapi/models/summary.go:110.26,111.30 1 1
|
||||
github.com/muety/wakapi/models/summary.go:111.30,113.4 1 1
|
||||
github.com/muety/wakapi/models/summary.go:117.37,119.3 1 0
|
||||
github.com/muety/wakapi/models/summary.go:124.33,130.3 1 1
|
||||
github.com/muety/wakapi/models/summary.go:133.45,138.30 3 1
|
||||
github.com/muety/wakapi/models/summary.go:147.2,147.30 1 1
|
||||
github.com/muety/wakapi/models/summary.go:138.30,139.47 1 1
|
||||
github.com/muety/wakapi/models/summary.go:139.47,140.32 1 1
|
||||
github.com/muety/wakapi/models/summary.go:143.4,143.9 1 1
|
||||
github.com/muety/wakapi/models/summary.go:140.32,142.5 1 1
|
||||
github.com/muety/wakapi/models/summary.go:150.73,152.55 2 1
|
||||
github.com/muety/wakapi/models/summary.go:157.2,157.16 1 1
|
||||
github.com/muety/wakapi/models/summary.go:152.55,153.31 1 1
|
||||
github.com/muety/wakapi/models/summary.go:153.31,155.4 1 1
|
||||
github.com/muety/wakapi/models/summary.go:160.88,162.55 2 1
|
||||
github.com/muety/wakapi/models/summary.go:170.2,170.16 1 1
|
||||
github.com/muety/wakapi/models/summary.go:162.55,163.31 1 1
|
||||
github.com/muety/wakapi/models/summary.go:163.31,164.23 1 1
|
||||
github.com/muety/wakapi/models/summary.go:167.4,167.46 1 1
|
||||
github.com/muety/wakapi/models/summary.go:164.23,165.13 1 1
|
||||
github.com/muety/wakapi/models/summary.go:173.70,175.8 2 1
|
||||
github.com/muety/wakapi/models/summary.go:178.2,178.10 1 1
|
||||
github.com/muety/wakapi/models/summary.go:175.8,177.3 1 1
|
||||
github.com/muety/wakapi/models/summary.go:181.71,182.63 1 1
|
||||
github.com/muety/wakapi/models/summary.go:222.2,228.10 6 1
|
||||
github.com/muety/wakapi/models/summary.go:182.63,185.45 2 1
|
||||
github.com/muety/wakapi/models/summary.go:194.3,194.31 1 1
|
||||
github.com/muety/wakapi/models/summary.go:201.3,201.31 1 1
|
||||
github.com/muety/wakapi/models/summary.go:218.3,218.16 1 1
|
||||
github.com/muety/wakapi/models/summary.go:185.45,186.32 1 1
|
||||
github.com/muety/wakapi/models/summary.go:191.4,191.14 1 1
|
||||
github.com/muety/wakapi/models/summary.go:186.32,187.24 1 1
|
||||
github.com/muety/wakapi/models/summary.go:187.24,189.6 1 1
|
||||
github.com/muety/wakapi/models/summary.go:194.31,196.60 1 1
|
||||
github.com/muety/wakapi/models/summary.go:196.60,198.5 1 1
|
||||
github.com/muety/wakapi/models/summary.go:201.31,203.60 1 1
|
||||
github.com/muety/wakapi/models/summary.go:203.60,204.55 1 1
|
||||
github.com/muety/wakapi/models/summary.go:204.55,206.6 1 1
|
||||
github.com/muety/wakapi/models/summary.go:206.11,214.6 1 1
|
||||
github.com/muety/wakapi/models/summary.go:231.33,233.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:235.43,237.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:239.38,241.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:68.29,70.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:72.37,79.2 6 1
|
||||
github.com/muety/wakapi/models/summary.go:81.35,83.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:85.57,93.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:95.64,97.2 1 0
|
||||
github.com/muety/wakapi/models/summary.go:110.33,115.26 4 1
|
||||
github.com/muety/wakapi/models/summary.go:122.2,122.37 1 1
|
||||
github.com/muety/wakapi/models/summary.go:126.2,129.33 2 1
|
||||
github.com/muety/wakapi/models/summary.go:115.26,116.30 1 1
|
||||
github.com/muety/wakapi/models/summary.go:116.30,118.4 1 1
|
||||
github.com/muety/wakapi/models/summary.go:122.37,124.3 1 0
|
||||
github.com/muety/wakapi/models/summary.go:129.33,135.3 1 1
|
||||
github.com/muety/wakapi/models/summary.go:138.45,143.30 3 1
|
||||
github.com/muety/wakapi/models/summary.go:152.2,152.30 1 1
|
||||
github.com/muety/wakapi/models/summary.go:143.30,144.47 1 1
|
||||
github.com/muety/wakapi/models/summary.go:144.47,145.32 1 1
|
||||
github.com/muety/wakapi/models/summary.go:148.4,148.9 1 1
|
||||
github.com/muety/wakapi/models/summary.go:145.32,147.5 1 1
|
||||
github.com/muety/wakapi/models/summary.go:155.73,157.55 2 1
|
||||
github.com/muety/wakapi/models/summary.go:162.2,162.16 1 1
|
||||
github.com/muety/wakapi/models/summary.go:157.55,158.31 1 1
|
||||
github.com/muety/wakapi/models/summary.go:158.31,160.4 1 1
|
||||
github.com/muety/wakapi/models/summary.go:165.88,167.55 2 1
|
||||
github.com/muety/wakapi/models/summary.go:175.2,175.16 1 1
|
||||
github.com/muety/wakapi/models/summary.go:167.55,168.31 1 1
|
||||
github.com/muety/wakapi/models/summary.go:168.31,169.23 1 1
|
||||
github.com/muety/wakapi/models/summary.go:172.4,172.46 1 1
|
||||
github.com/muety/wakapi/models/summary.go:169.23,170.13 1 1
|
||||
github.com/muety/wakapi/models/summary.go:178.70,180.8 2 1
|
||||
github.com/muety/wakapi/models/summary.go:183.2,183.10 1 1
|
||||
github.com/muety/wakapi/models/summary.go:180.8,182.3 1 1
|
||||
github.com/muety/wakapi/models/summary.go:186.71,187.63 1 1
|
||||
github.com/muety/wakapi/models/summary.go:227.2,233.10 6 1
|
||||
github.com/muety/wakapi/models/summary.go:187.63,190.45 2 1
|
||||
github.com/muety/wakapi/models/summary.go:199.3,199.31 1 1
|
||||
github.com/muety/wakapi/models/summary.go:206.3,206.31 1 1
|
||||
github.com/muety/wakapi/models/summary.go:223.3,223.16 1 1
|
||||
github.com/muety/wakapi/models/summary.go:190.45,191.32 1 1
|
||||
github.com/muety/wakapi/models/summary.go:196.4,196.14 1 1
|
||||
github.com/muety/wakapi/models/summary.go:191.32,192.24 1 1
|
||||
github.com/muety/wakapi/models/summary.go:192.24,194.6 1 1
|
||||
github.com/muety/wakapi/models/summary.go:199.31,201.60 1 1
|
||||
github.com/muety/wakapi/models/summary.go:201.60,203.5 1 1
|
||||
github.com/muety/wakapi/models/summary.go:206.31,208.60 1 1
|
||||
github.com/muety/wakapi/models/summary.go:208.60,209.55 1 1
|
||||
github.com/muety/wakapi/models/summary.go:209.55,211.6 1 1
|
||||
github.com/muety/wakapi/models/summary.go:211.11,219.6 1 1
|
||||
github.com/muety/wakapi/models/summary.go:236.33,238.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:240.43,242.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:244.38,246.2 1 1
|
||||
github.com/muety/wakapi/models/user.go:46.43,49.2 1 0
|
||||
github.com/muety/wakapi/models/user.go:51.33,55.2 1 0
|
||||
github.com/muety/wakapi/models/user.go:57.45,59.2 1 0
|
||||
github.com/muety/wakapi/models/user.go:61.45,63.2 1 0
|
||||
github.com/muety/wakapi/models/alias.go:12.32,14.2 1 0
|
||||
github.com/muety/wakapi/models/alias.go:16.37,17.35 1 0
|
||||
github.com/muety/wakapi/models/alias.go:22.2,22.14 1 0
|
||||
github.com/muety/wakapi/models/alias.go:17.35,18.18 1 0
|
||||
github.com/muety/wakapi/models/alias.go:18.18,20.4 1 0
|
||||
github.com/muety/wakapi/models/filters.go:16.56,17.16 1 0
|
||||
github.com/muety/wakapi/models/filters.go:29.2,29.19 1 0
|
||||
github.com/muety/wakapi/models/filters.go:18.22,19.32 1 0
|
||||
@ -125,93 +114,34 @@ github.com/muety/wakapi/models/interval.go:39.47,40.23 1 0
|
||||
github.com/muety/wakapi/models/interval.go:45.2,45.14 1 0
|
||||
github.com/muety/wakapi/models/interval.go:40.23,41.13 1 0
|
||||
github.com/muety/wakapi/models/interval.go:41.13,43.4 1 0
|
||||
github.com/muety/wakapi/models/user.go:40.43,43.2 1 0
|
||||
github.com/muety/wakapi/models/user.go:45.33,49.2 1 0
|
||||
github.com/muety/wakapi/models/user.go:51.45,53.2 1 0
|
||||
github.com/muety/wakapi/models/user.go:55.45,57.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:95.70,97.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:99.65,101.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:103.82,113.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:115.31,117.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:119.32,121.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:123.74,124.19 1 0
|
||||
github.com/muety/wakapi/config/config.go:125.10,126.34 1 0
|
||||
github.com/muety/wakapi/config/config.go:126.34,135.4 8 0
|
||||
github.com/muety/wakapi/config/config.go:139.73,140.33 1 0
|
||||
github.com/muety/wakapi/config/config.go:140.33,148.17 5 0
|
||||
github.com/muety/wakapi/config/config.go:152.3,153.13 2 0
|
||||
github.com/muety/wakapi/config/config.go:148.17,150.4 1 0
|
||||
github.com/muety/wakapi/config/config.go:157.50,158.19 1 0
|
||||
github.com/muety/wakapi/config/config.go:171.2,171.12 1 0
|
||||
github.com/muety/wakapi/config/config.go:159.23,163.5 1 0
|
||||
github.com/muety/wakapi/config/config.go:164.26,167.5 1 0
|
||||
github.com/muety/wakapi/config/config.go:168.24,169.48 1 0
|
||||
github.com/muety/wakapi/config/config.go:174.53,184.2 1 1
|
||||
github.com/muety/wakapi/config/config.go:186.56,188.16 2 1
|
||||
github.com/muety/wakapi/config/config.go:192.2,199.3 1 1
|
||||
github.com/muety/wakapi/config/config.go:188.16,190.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:202.54,204.2 1 1
|
||||
github.com/muety/wakapi/config/config.go:206.60,208.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:210.59,212.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:214.57,216.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:218.53,220.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:222.29,224.2 1 1
|
||||
github.com/muety/wakapi/config/config.go:226.27,228.16 2 0
|
||||
github.com/muety/wakapi/config/config.go:231.2,234.16 3 0
|
||||
github.com/muety/wakapi/config/config.go:238.2,238.41 1 0
|
||||
github.com/muety/wakapi/config/config.go:228.16,230.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:234.16,236.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:241.48,253.16 3 0
|
||||
github.com/muety/wakapi/config/config.go:256.2,258.16 3 0
|
||||
github.com/muety/wakapi/config/config.go:262.2,262.55 1 0
|
||||
github.com/muety/wakapi/config/config.go:266.2,266.15 1 0
|
||||
github.com/muety/wakapi/config/config.go:253.16,255.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:258.16,260.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:262.55,264.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:269.38,270.43 1 0
|
||||
github.com/muety/wakapi/config/config.go:273.2,273.15 1 0
|
||||
github.com/muety/wakapi/config/config.go:270.43,272.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:276.45,277.27 1 0
|
||||
github.com/muety/wakapi/config/config.go:280.2,280.15 1 0
|
||||
github.com/muety/wakapi/config/config.go:277.27,279.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:283.26,285.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:287.20,289.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:291.21,296.96 3 0
|
||||
github.com/muety/wakapi/config/config.go:300.2,308.52 5 0
|
||||
github.com/muety/wakapi/config/config.go:312.2,312.47 1 0
|
||||
github.com/muety/wakapi/config/config.go:318.2,318.70 1 0
|
||||
github.com/muety/wakapi/config/config.go:322.2,322.28 1 0
|
||||
github.com/muety/wakapi/config/config.go:326.2,327.14 2 0
|
||||
github.com/muety/wakapi/config/config.go:296.96,298.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:308.52,310.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:312.47,313.14 1 0
|
||||
github.com/muety/wakapi/config/config.go:313.14,315.4 1 0
|
||||
github.com/muety/wakapi/config/config.go:318.70,320.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:322.28,324.3 1 0
|
||||
github.com/muety/wakapi/config/utils.go:5.78,7.22 2 0
|
||||
github.com/muety/wakapi/config/utils.go:13.2,13.11 1 0
|
||||
github.com/muety/wakapi/config/utils.go:7.22,8.18 1 0
|
||||
github.com/muety/wakapi/config/utils.go:11.3,11.12 1 0
|
||||
github.com/muety/wakapi/config/utils.go:8.18,10.4 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:16.79,18.54 2 0
|
||||
github.com/muety/wakapi/utils/auth.go:22.2,24.16 3 0
|
||||
github.com/muety/wakapi/utils/auth.go:28.2,30.45 3 0
|
||||
github.com/muety/wakapi/utils/auth.go:33.2,34.32 2 0
|
||||
github.com/muety/wakapi/utils/auth.go:18.54,20.3 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:24.16,26.3 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:30.45,32.3 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:37.65,39.54 2 0
|
||||
github.com/muety/wakapi/utils/auth.go:43.2,44.30 2 0
|
||||
github.com/muety/wakapi/utils/auth.go:39.54,41.3 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:47.94,49.16 2 0
|
||||
github.com/muety/wakapi/utils/auth.go:53.2,53.107 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:57.2,57.22 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:49.16,51.3 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:53.107,55.3 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:60.56,64.2 3 0
|
||||
github.com/muety/wakapi/utils/auth.go:66.55,69.16 3 0
|
||||
github.com/muety/wakapi/utils/auth.go:72.2,72.16 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:69.16,71.3 1 0
|
||||
github.com/muety/wakapi/models/models.go:3.14,5.2 0 1
|
||||
github.com/muety/wakapi/models/shared.go:35.52,37.2 1 0
|
||||
github.com/muety/wakapi/models/shared.go:39.52,42.16 3 0
|
||||
github.com/muety/wakapi/models/shared.go:45.2,47.12 3 0
|
||||
github.com/muety/wakapi/models/shared.go:42.16,44.3 1 0
|
||||
github.com/muety/wakapi/models/shared.go:51.52,57.22 2 0
|
||||
github.com/muety/wakapi/models/shared.go:73.2,76.12 3 0
|
||||
github.com/muety/wakapi/models/shared.go:58.14,60.17 2 0
|
||||
github.com/muety/wakapi/models/shared.go:63.13,65.8 2 0
|
||||
github.com/muety/wakapi/models/shared.go:66.17,68.8 2 0
|
||||
github.com/muety/wakapi/models/shared.go:69.10,70.64 1 0
|
||||
github.com/muety/wakapi/models/shared.go:60.17,62.4 1 0
|
||||
github.com/muety/wakapi/models/shared.go:79.45,81.2 1 0
|
||||
github.com/muety/wakapi/models/shared.go:83.51,86.2 2 0
|
||||
github.com/muety/wakapi/models/shared.go:88.37,91.2 2 0
|
||||
github.com/muety/wakapi/models/shared.go:93.35,95.2 1 0
|
||||
github.com/muety/wakapi/models/shared.go:97.34,99.2 1 0
|
||||
github.com/muety/wakapi/utils/color.go:8.90,10.32 2 0
|
||||
github.com/muety/wakapi/utils/color.go:15.2,15.15 1 0
|
||||
github.com/muety/wakapi/utils/color.go:10.32,11.50 1 0
|
||||
github.com/muety/wakapi/utils/color.go:11.50,13.4 1 0
|
||||
github.com/muety/wakapi/utils/common.go:10.48,12.2 1 0
|
||||
github.com/muety/wakapi/utils/common.go:14.40,16.2 1 0
|
||||
github.com/muety/wakapi/utils/common.go:18.45,20.2 1 0
|
||||
github.com/muety/wakapi/utils/common.go:22.24,24.2 1 0
|
||||
github.com/muety/wakapi/utils/common.go:26.56,29.45 3 1
|
||||
github.com/muety/wakapi/utils/common.go:32.2,32.40 1 1
|
||||
github.com/muety/wakapi/utils/common.go:29.45,31.3 1 1
|
||||
github.com/muety/wakapi/utils/date.go:8.31,10.2 1 0
|
||||
github.com/muety/wakapi/utils/date.go:12.43,14.2 1 0
|
||||
github.com/muety/wakapi/utils/date.go:16.30,20.2 3 0
|
||||
@ -230,28 +160,33 @@ github.com/muety/wakapi/utils/date.go:71.2,71.13 1 0
|
||||
github.com/muety/wakapi/utils/date.go:59.36,62.3 2 0
|
||||
github.com/muety/wakapi/utils/date.go:63.21,66.3 2 0
|
||||
github.com/muety/wakapi/utils/date.go:67.21,70.3 2 0
|
||||
github.com/muety/wakapi/utils/strings.go:8.34,10.2 1 0
|
||||
github.com/muety/wakapi/utils/strings.go:12.77,13.29 1 0
|
||||
github.com/muety/wakapi/utils/strings.go:18.2,18.19 1 0
|
||||
github.com/muety/wakapi/utils/strings.go:13.29,14.18 1 0
|
||||
github.com/muety/wakapi/utils/strings.go:14.18,16.4 1 0
|
||||
github.com/muety/wakapi/utils/http.go:9.73,12.58 3 0
|
||||
github.com/muety/wakapi/utils/http.go:12.58,14.3 1 0
|
||||
github.com/muety/wakapi/utils/template.go:8.41,10.16 2 0
|
||||
github.com/muety/wakapi/utils/template.go:13.2,13.23 1 0
|
||||
github.com/muety/wakapi/utils/template.go:10.16,12.3 1 0
|
||||
github.com/muety/wakapi/utils/template.go:16.37,17.30 1 0
|
||||
github.com/muety/wakapi/utils/template.go:20.2,20.10 1 0
|
||||
github.com/muety/wakapi/utils/template.go:17.30,19.3 1 0
|
||||
github.com/muety/wakapi/utils/color.go:8.90,10.32 2 0
|
||||
github.com/muety/wakapi/utils/color.go:15.2,15.15 1 0
|
||||
github.com/muety/wakapi/utils/color.go:10.32,11.50 1 0
|
||||
github.com/muety/wakapi/utils/color.go:11.50,13.4 1 0
|
||||
github.com/muety/wakapi/utils/common.go:9.48,11.2 1 0
|
||||
github.com/muety/wakapi/utils/common.go:13.40,15.2 1 0
|
||||
github.com/muety/wakapi/utils/common.go:17.45,19.2 1 0
|
||||
github.com/muety/wakapi/utils/common.go:21.24,23.2 1 0
|
||||
github.com/muety/wakapi/utils/common.go:25.56,28.45 3 1
|
||||
github.com/muety/wakapi/utils/common.go:31.2,31.40 1 1
|
||||
github.com/muety/wakapi/utils/common.go:28.45,30.3 1 1
|
||||
github.com/muety/wakapi/utils/auth.go:16.79,18.54 2 0
|
||||
github.com/muety/wakapi/utils/auth.go:22.2,24.16 3 0
|
||||
github.com/muety/wakapi/utils/auth.go:28.2,30.45 3 0
|
||||
github.com/muety/wakapi/utils/auth.go:33.2,34.32 2 0
|
||||
github.com/muety/wakapi/utils/auth.go:18.54,20.3 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:24.16,26.3 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:30.45,32.3 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:37.65,39.85 2 0
|
||||
github.com/muety/wakapi/utils/auth.go:43.2,44.30 2 0
|
||||
github.com/muety/wakapi/utils/auth.go:39.85,41.3 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:47.94,49.16 2 0
|
||||
github.com/muety/wakapi/utils/auth.go:53.2,53.107 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:57.2,57.22 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:49.16,51.3 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:53.107,55.3 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:60.56,64.2 3 0
|
||||
github.com/muety/wakapi/utils/auth.go:66.55,69.16 3 0
|
||||
github.com/muety/wakapi/utils/auth.go:72.2,72.16 1 0
|
||||
github.com/muety/wakapi/utils/auth.go:69.16,71.3 1 0
|
||||
github.com/muety/wakapi/utils/filesystem.go:14.68,16.16 2 0
|
||||
github.com/muety/wakapi/utils/filesystem.go:20.2,21.15 2 0
|
||||
github.com/muety/wakapi/utils/filesystem.go:33.2,33.15 1 0
|
||||
@ -260,156 +195,191 @@ github.com/muety/wakapi/utils/filesystem.go:21.15,23.47 2 0
|
||||
github.com/muety/wakapi/utils/filesystem.go:23.47,25.23 2 0
|
||||
github.com/muety/wakapi/utils/filesystem.go:29.4,29.19 1 0
|
||||
github.com/muety/wakapi/utils/filesystem.go:25.23,27.5 1 0
|
||||
github.com/muety/wakapi/utils/http.go:9.73,12.58 3 0
|
||||
github.com/muety/wakapi/utils/http.go:12.58,14.3 1 0
|
||||
github.com/muety/wakapi/utils/strings.go:8.34,10.2 1 0
|
||||
github.com/muety/wakapi/utils/strings.go:12.77,13.29 1 0
|
||||
github.com/muety/wakapi/utils/strings.go:18.2,18.19 1 0
|
||||
github.com/muety/wakapi/utils/strings.go:13.29,14.18 1 0
|
||||
github.com/muety/wakapi/utils/strings.go:14.18,16.4 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:10.66,11.40 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:16.2,16.48 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:11.40,12.27 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:12.27,14.4 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:19.74,21.16 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:24.2,24.32 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:21.16,23.3 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:27.84,30.18 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:65.2,65.22 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:31.28,32.24 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:33.32,35.22 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:36.31,37.23 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:38.31,40.21 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:41.32,42.24 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:43.32,45.22 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:46.31,47.23 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:48.32,49.42 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:50.41,52.40 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:53.33,54.43 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:55.33,56.43 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:57.35,58.43 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:59.26,60.21 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:61.10,62.39 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:68.73,75.56 5 0
|
||||
github.com/muety/wakapi/utils/summary.go:89.2,96.8 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:75.56,77.3 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:77.8,79.17 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:83.3,84.17 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:79.17,81.4 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:84.17,86.4 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:19.67,22.2 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:24.74,26.16 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:29.2,29.32 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:26.16,28.3 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:32.84,35.18 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:70.2,70.22 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:36.28,37.24 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:38.32,40.22 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:41.31,42.23 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:43.31,45.21 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:46.32,47.24 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:48.32,50.22 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:51.31,52.23 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:53.32,54.42 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:55.41,57.40 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:58.33,59.43 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:60.33,61.43 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:62.35,63.43 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:64.26,65.21 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:66.10,67.39 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:73.73,80.56 5 0
|
||||
github.com/muety/wakapi/utils/summary.go:96.2,103.8 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:80.56,82.3 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:82.8,82.54 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:82.54,84.3 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:84.8,86.17 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:90.3,91.17 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:86.17,88.4 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:91.17,93.4 1 0
|
||||
github.com/muety/wakapi/config/config.go:103.70,105.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:107.65,109.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:111.82,121.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:123.31,125.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:127.32,129.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:131.74,132.19 1 0
|
||||
github.com/muety/wakapi/config/config.go:133.10,134.34 1 0
|
||||
github.com/muety/wakapi/config/config.go:134.34,143.4 8 0
|
||||
github.com/muety/wakapi/config/config.go:147.73,148.33 1 0
|
||||
github.com/muety/wakapi/config/config.go:148.33,156.17 5 0
|
||||
github.com/muety/wakapi/config/config.go:160.3,161.13 2 0
|
||||
github.com/muety/wakapi/config/config.go:156.17,158.4 1 0
|
||||
github.com/muety/wakapi/config/config.go:165.50,166.19 1 0
|
||||
github.com/muety/wakapi/config/config.go:179.2,179.12 1 0
|
||||
github.com/muety/wakapi/config/config.go:167.23,171.5 1 0
|
||||
github.com/muety/wakapi/config/config.go:172.26,175.5 1 0
|
||||
github.com/muety/wakapi/config/config.go:176.24,177.48 1 0
|
||||
github.com/muety/wakapi/config/config.go:182.53,192.2 1 1
|
||||
github.com/muety/wakapi/config/config.go:194.56,196.16 2 1
|
||||
github.com/muety/wakapi/config/config.go:200.2,207.3 1 1
|
||||
github.com/muety/wakapi/config/config.go:196.16,198.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:210.54,212.2 1 1
|
||||
github.com/muety/wakapi/config/config.go:214.60,216.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:218.59,220.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:222.57,224.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:226.53,228.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:230.29,232.2 1 1
|
||||
github.com/muety/wakapi/config/config.go:234.27,236.16 2 0
|
||||
github.com/muety/wakapi/config/config.go:239.2,242.16 3 0
|
||||
github.com/muety/wakapi/config/config.go:246.2,246.41 1 0
|
||||
github.com/muety/wakapi/config/config.go:236.16,238.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:242.16,244.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:249.48,261.16 3 0
|
||||
github.com/muety/wakapi/config/config.go:264.2,266.16 3 0
|
||||
github.com/muety/wakapi/config/config.go:270.2,270.55 1 0
|
||||
github.com/muety/wakapi/config/config.go:274.2,274.15 1 0
|
||||
github.com/muety/wakapi/config/config.go:261.16,263.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:266.16,268.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:270.55,272.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:277.38,278.43 1 0
|
||||
github.com/muety/wakapi/config/config.go:281.2,281.15 1 0
|
||||
github.com/muety/wakapi/config/config.go:278.43,280.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:284.45,285.27 1 0
|
||||
github.com/muety/wakapi/config/config.go:288.2,288.15 1 0
|
||||
github.com/muety/wakapi/config/config.go:285.27,287.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:291.26,293.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:295.20,297.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:299.21,304.96 3 0
|
||||
github.com/muety/wakapi/config/config.go:308.2,316.52 5 0
|
||||
github.com/muety/wakapi/config/config.go:320.2,320.47 1 0
|
||||
github.com/muety/wakapi/config/config.go:326.2,326.70 1 0
|
||||
github.com/muety/wakapi/config/config.go:330.2,330.28 1 0
|
||||
github.com/muety/wakapi/config/config.go:334.2,335.14 2 0
|
||||
github.com/muety/wakapi/config/config.go:304.96,306.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:316.52,318.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:320.47,321.14 1 0
|
||||
github.com/muety/wakapi/config/config.go:321.14,323.4 1 0
|
||||
github.com/muety/wakapi/config/config.go:326.70,328.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:330.28,332.3 1 0
|
||||
github.com/muety/wakapi/config/utils.go:5.78,7.22 2 0
|
||||
github.com/muety/wakapi/config/utils.go:13.2,13.11 1 0
|
||||
github.com/muety/wakapi/config/utils.go:7.22,8.18 1 0
|
||||
github.com/muety/wakapi/config/utils.go:11.3,11.12 1 0
|
||||
github.com/muety/wakapi/config/utils.go:8.18,10.4 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:20.91,26.2 1 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:28.90,31.2 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:33.71,34.71 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:34.71,36.3 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:39.107,43.16 3 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:47.2,47.31 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:62.2,63.29 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:43.16,45.3 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:47.31,48.31 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:53.3,53.44 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:59.3,59.9 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:48.31,51.4 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:53.44,55.4 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:55.9,58.4 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:66.70,67.39 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:72.2,72.14 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:67.39,68.60 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:68.60,70.4 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:75.92,77.16 2 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:81.2,84.16 4 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:87.2,87.18 1 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:77.16,79.3 1 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:84.16,86.3 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:90.92,92.16 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:96.2,97.16 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:104.2,104.18 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:92.16,94.3 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:97.16,99.3 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:33.90,36.2 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:38.71,39.71 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:39.71,41.3 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:44.107,48.16 3 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:52.2,52.31 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:68.2,69.29 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:48.16,50.3 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:52.31,53.31 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:58.3,58.29 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:65.3,65.9 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:53.31,56.4 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:58.29,61.4 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:61.9,64.4 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:72.70,73.39 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:78.2,78.14 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:73.39,74.60 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:74.60,76.4 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:81.92,83.16 2 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:87.2,90.16 4 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:93.2,93.18 1 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:83.16,85.3 1 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:90.16,92.3 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:96.92,98.16 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:102.2,103.16 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:110.2,110.18 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:98.16,100.3 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:103.16,105.3 1 0
|
||||
github.com/muety/wakapi/middlewares/filetype.go:13.83,14.43 1 0
|
||||
github.com/muety/wakapi/middlewares/filetype.go:14.43,19.3 1 0
|
||||
github.com/muety/wakapi/middlewares/filetype.go:22.84,24.34 2 0
|
||||
github.com/muety/wakapi/middlewares/filetype.go:31.2,31.27 1 0
|
||||
github.com/muety/wakapi/middlewares/filetype.go:24.34,25.50 1 0
|
||||
github.com/muety/wakapi/middlewares/filetype.go:25.50,29.4 3 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:17.79,18.43 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:18.43,23.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:26.80,44.2 6 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:46.41,48.14 2 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:51.2,51.14 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:54.2,54.11 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:48.14,50.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:51.14,53.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:85.52,87.2 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:99.45,100.20 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:100.20,104.3 3 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:106.54,109.18 3 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:116.2,117.15 2 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:109.18,112.17 2 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:112.17,114.4 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:119.42,120.20 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:120.20,122.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:124.36,126.2 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:127.42,129.2 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:130.40,132.2 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:133.52,135.2 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:19.105,20.43 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:20.43,26.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:29.80,38.44 7 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:44.2,53.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:38.44,39.38 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:39.38,41.4 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:56.41,58.14 2 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:61.2,61.14 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:64.2,64.11 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:58.14,60.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:61.14,63.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:95.52,97.2 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:109.45,110.20 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:110.20,114.3 3 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:116.54,119.18 3 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:126.2,127.15 2 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:119.18,122.17 2 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:122.17,124.4 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:129.42,130.20 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:130.20,132.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:134.36,136.2 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:137.42,139.2 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:140.40,142.2 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:143.52,145.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:17.141,23.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:25.72,27.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:29.80,31.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:33.76,35.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:37.111,39.16 2 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:42.2,42.43 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:39.16,41.3 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:45.116,47.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:49.78,51.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:53.62,55.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:57.116,59.16 2 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:63.2,63.28 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:67.2,67.24 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:59.16,61.3 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:63.28,65.3 1 0
|
||||
github.com/muety/wakapi/services/misc.go:23.126,30.2 1 0
|
||||
github.com/muety/wakapi/services/misc.go:42.50,44.48 1 0
|
||||
github.com/muety/wakapi/services/misc.go:48.2,50.19 3 0
|
||||
github.com/muety/wakapi/services/misc.go:44.48,46.3 1 0
|
||||
github.com/muety/wakapi/services/misc.go:53.51,59.40 4 0
|
||||
github.com/muety/wakapi/services/misc.go:63.2,66.56 2 0
|
||||
github.com/muety/wakapi/services/misc.go:77.2,77.12 1 0
|
||||
github.com/muety/wakapi/services/misc.go:59.40,61.3 1 0
|
||||
github.com/muety/wakapi/services/misc.go:66.56,67.27 1 0
|
||||
github.com/muety/wakapi/services/misc.go:67.27,72.4 1 0
|
||||
github.com/muety/wakapi/services/misc.go:73.8,75.3 1 0
|
||||
github.com/muety/wakapi/services/misc.go:80.116,81.24 1 0
|
||||
github.com/muety/wakapi/services/misc.go:81.24,82.144 1 0
|
||||
github.com/muety/wakapi/services/misc.go:91.3,91.48 1 0
|
||||
github.com/muety/wakapi/services/misc.go:82.144,84.4 1 0
|
||||
github.com/muety/wakapi/services/misc.go:84.9,90.4 2 0
|
||||
github.com/muety/wakapi/services/misc.go:91.48,94.4 2 0
|
||||
github.com/muety/wakapi/services/misc.go:98.86,101.30 3 0
|
||||
github.com/muety/wakapi/services/misc.go:106.2,109.17 1 0
|
||||
github.com/muety/wakapi/services/misc.go:113.2,116.17 1 0
|
||||
github.com/muety/wakapi/services/misc.go:101.30,104.3 2 0
|
||||
github.com/muety/wakapi/services/misc.go:109.17,111.3 1 0
|
||||
github.com/muety/wakapi/services/misc.go:116.17,118.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:19.73,25.2 1 0
|
||||
github.com/muety/wakapi/services/user.go:27.74,28.40 1 0
|
||||
github.com/muety/wakapi/services/user.go:32.2,33.16 2 0
|
||||
github.com/muety/wakapi/services/user.go:37.2,38.15 2 0
|
||||
github.com/muety/wakapi/services/user.go:28.40,30.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:33.16,35.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:41.72,42.37 1 0
|
||||
github.com/muety/wakapi/services/user.go:46.2,47.16 2 0
|
||||
github.com/muety/wakapi/services/user.go:51.2,52.15 2 0
|
||||
github.com/muety/wakapi/services/user.go:42.37,44.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:47.16,49.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:55.58,57.2 1 0
|
||||
github.com/muety/wakapi/services/user.go:59.88,66.93 2 0
|
||||
github.com/muety/wakapi/services/user.go:72.2,72.38 1 0
|
||||
github.com/muety/wakapi/services/user.go:66.93,68.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:68.8,70.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:75.73,78.2 2 0
|
||||
github.com/muety/wakapi/services/user.go:80.78,84.2 3 0
|
||||
github.com/muety/wakapi/services/user.go:86.99,89.2 2 0
|
||||
github.com/muety/wakapi/services/user.go:91.106,94.96 3 0
|
||||
github.com/muety/wakapi/services/user.go:99.2,99.68 1 0
|
||||
github.com/muety/wakapi/services/user.go:94.96,96.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:96.8,98.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:102.57,105.2 2 0
|
||||
github.com/muety/wakapi/services/user.go:107.38,109.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:33.53,35.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:37.76,39.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:41.96,43.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:45.111,47.16 2 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:50.2,50.43 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:47.16,49.3 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:53.116,55.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:57.78,59.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:61.62,63.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:65.116,67.16 2 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:71.2,71.28 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:75.2,75.24 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:67.16,69.3 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:71.28,73.3 1 0
|
||||
github.com/muety/wakapi/services/key_value.go:14.89,19.2 1 0
|
||||
github.com/muety/wakapi/services/key_value.go:21.83,23.2 1 0
|
||||
github.com/muety/wakapi/services/key_value.go:25.78,27.16 2 0
|
||||
github.com/muety/wakapi/services/key_value.go:33.2,33.11 1 0
|
||||
github.com/muety/wakapi/services/key_value.go:27.16,32.3 1 0
|
||||
github.com/muety/wakapi/services/key_value.go:36.72,38.2 1 0
|
||||
github.com/muety/wakapi/services/key_value.go:40.60,42.2 1 0
|
||||
github.com/muety/wakapi/services/aggregation.go:24.142,31.2 1 0
|
||||
github.com/muety/wakapi/services/aggregation.go:40.43,42.37 1 0
|
||||
github.com/muety/wakapi/services/aggregation.go:46.2,48.19 3 0
|
||||
@ -485,32 +455,6 @@ github.com/muety/wakapi/services/alias.go:95.21,97.4 1 0
|
||||
github.com/muety/wakapi/services/alias.go:104.31,106.3 1 0
|
||||
github.com/muety/wakapi/services/alias.go:111.52,112.51 1 0
|
||||
github.com/muety/wakapi/services/alias.go:112.51,114.3 1 0
|
||||
github.com/muety/wakapi/services/key_value.go:14.89,19.2 1 0
|
||||
github.com/muety/wakapi/services/key_value.go:21.83,23.2 1 0
|
||||
github.com/muety/wakapi/services/key_value.go:25.78,27.16 2 0
|
||||
github.com/muety/wakapi/services/key_value.go:33.2,33.11 1 0
|
||||
github.com/muety/wakapi/services/key_value.go:27.16,32.3 1 0
|
||||
github.com/muety/wakapi/services/key_value.go:36.72,38.2 1 0
|
||||
github.com/muety/wakapi/services/key_value.go:40.60,42.2 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:18.118,24.2 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:26.86,28.2 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:30.96,31.53 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:35.2,36.16 2 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:39.2,40.22 2 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:31.53,33.3 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:36.16,38.3 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:43.92,46.16 3 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:50.2,50.33 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:53.2,53.22 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:46.16,48.3 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:50.33,52.3 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:56.109,58.16 2 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:62.2,63.20 2 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:58.16,60.3 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:66.82,67.26 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:70.2,72.12 3 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:67.26,69.3 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:75.74,78.2 1 0
|
||||
github.com/muety/wakapi/services/summary.go:27.149,35.2 1 1
|
||||
github.com/muety/wakapi/services/summary.go:39.120,42.52 2 1
|
||||
github.com/muety/wakapi/services/summary.go:47.2,47.44 1 1
|
||||
@ -600,3 +544,72 @@ github.com/muety/wakapi/services/summary.go:324.54,326.3 1 1
|
||||
github.com/muety/wakapi/services/summary.go:331.59,333.25 2 1
|
||||
github.com/muety/wakapi/services/summary.go:336.2,336.32 1 1
|
||||
github.com/muety/wakapi/services/summary.go:333.25,335.3 1 1
|
||||
github.com/muety/wakapi/services/user.go:19.73,25.2 1 0
|
||||
github.com/muety/wakapi/services/user.go:27.74,28.40 1 0
|
||||
github.com/muety/wakapi/services/user.go:32.2,33.16 2 0
|
||||
github.com/muety/wakapi/services/user.go:37.2,38.15 2 0
|
||||
github.com/muety/wakapi/services/user.go:28.40,30.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:33.16,35.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:41.72,42.37 1 0
|
||||
github.com/muety/wakapi/services/user.go:46.2,47.16 2 0
|
||||
github.com/muety/wakapi/services/user.go:51.2,52.15 2 0
|
||||
github.com/muety/wakapi/services/user.go:42.37,44.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:47.16,49.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:55.58,57.2 1 0
|
||||
github.com/muety/wakapi/services/user.go:59.61,62.2 2 0
|
||||
github.com/muety/wakapi/services/user.go:64.48,66.2 1 0
|
||||
github.com/muety/wakapi/services/user.go:68.102,76.93 2 0
|
||||
github.com/muety/wakapi/services/user.go:82.2,82.38 1 0
|
||||
github.com/muety/wakapi/services/user.go:76.93,78.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:78.8,80.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:85.73,88.2 2 0
|
||||
github.com/muety/wakapi/services/user.go:90.78,94.2 3 0
|
||||
github.com/muety/wakapi/services/user.go:96.99,99.2 2 0
|
||||
github.com/muety/wakapi/services/user.go:101.106,104.96 3 0
|
||||
github.com/muety/wakapi/services/user.go:109.2,109.68 1 0
|
||||
github.com/muety/wakapi/services/user.go:104.96,106.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:106.8,108.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:112.57,115.2 2 0
|
||||
github.com/muety/wakapi/services/user.go:117.38,119.2 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:18.118,24.2 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:26.86,28.2 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:30.96,31.53 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:35.2,36.16 2 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:39.2,40.22 2 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:31.53,33.3 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:36.16,38.3 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:43.92,46.16 3 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:50.2,50.33 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:53.2,53.22 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:46.16,48.3 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:50.33,52.3 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:56.109,58.16 2 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:62.2,63.20 2 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:58.16,60.3 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:66.82,67.26 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:70.2,72.12 3 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:67.26,69.3 1 0
|
||||
github.com/muety/wakapi/services/language_mapping.go:75.74,78.2 1 0
|
||||
github.com/muety/wakapi/services/misc.go:23.126,30.2 1 0
|
||||
github.com/muety/wakapi/services/misc.go:42.50,44.48 1 0
|
||||
github.com/muety/wakapi/services/misc.go:48.2,50.19 3 0
|
||||
github.com/muety/wakapi/services/misc.go:44.48,46.3 1 0
|
||||
github.com/muety/wakapi/services/misc.go:53.51,59.40 4 0
|
||||
github.com/muety/wakapi/services/misc.go:63.2,66.56 2 0
|
||||
github.com/muety/wakapi/services/misc.go:77.2,77.12 1 0
|
||||
github.com/muety/wakapi/services/misc.go:59.40,61.3 1 0
|
||||
github.com/muety/wakapi/services/misc.go:66.56,67.27 1 0
|
||||
github.com/muety/wakapi/services/misc.go:67.27,72.4 1 0
|
||||
github.com/muety/wakapi/services/misc.go:73.8,75.3 1 0
|
||||
github.com/muety/wakapi/services/misc.go:80.116,81.24 1 0
|
||||
github.com/muety/wakapi/services/misc.go:81.24,82.144 1 0
|
||||
github.com/muety/wakapi/services/misc.go:91.3,91.48 1 0
|
||||
github.com/muety/wakapi/services/misc.go:82.144,84.4 1 0
|
||||
github.com/muety/wakapi/services/misc.go:84.9,90.4 2 0
|
||||
github.com/muety/wakapi/services/misc.go:91.48,94.4 2 0
|
||||
github.com/muety/wakapi/services/misc.go:98.86,101.30 3 0
|
||||
github.com/muety/wakapi/services/misc.go:106.2,109.17 1 0
|
||||
github.com/muety/wakapi/services/misc.go:113.2,116.17 1 0
|
||||
github.com/muety/wakapi/services/misc.go:101.30,104.3 2 0
|
||||
github.com/muety/wakapi/services/misc.go:109.17,111.3 1 0
|
||||
github.com/muety/wakapi/services/misc.go:116.17,118.3 1 0
|
||||
|
@ -49,6 +49,7 @@
|
||||
"DataWeave": "#003a52",
|
||||
"DM": "#447265",
|
||||
"Dockerfile": "#384d54",
|
||||
"Docker": "#384d54",
|
||||
"Dogescript": "#cca760",
|
||||
"Dylan": "#6c616e",
|
||||
"E": "#ccce35",
|
||||
|
7
main.go
7
main.go
@ -149,6 +149,7 @@ func main() {
|
||||
healthApiHandler := api.NewHealthApiHandler(db)
|
||||
heartbeatApiHandler := api.NewHeartbeatApiHandler(userService, heartbeatService, languageMappingService)
|
||||
summaryApiHandler := api.NewSummaryApiHandler(userService, summaryService)
|
||||
metricsHandler := api.NewMetricsHandler(userService, summaryService, heartbeatService, keyValueService)
|
||||
|
||||
// Compat Handlers
|
||||
wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(userService, summaryService)
|
||||
@ -170,10 +171,7 @@ func main() {
|
||||
|
||||
// Globally used middlewares
|
||||
recoveryMiddleware := handlers.RecoveryHandler()
|
||||
loggingMiddleware := middlewares.NewLoggingMiddleware(
|
||||
log.New(os.Stdout, "", log.LstdFlags),
|
||||
[]string{"/assets"},
|
||||
)
|
||||
loggingMiddleware := middlewares.NewLoggingMiddleware(logbuch.Info, []string{"/assets"})
|
||||
|
||||
// Router configs
|
||||
router.Use(loggingMiddleware, recoveryMiddleware)
|
||||
@ -189,6 +187,7 @@ func main() {
|
||||
summaryApiHandler.RegisterRoutes(apiRouter)
|
||||
healthApiHandler.RegisterRoutes(apiRouter)
|
||||
heartbeatApiHandler.RegisterRoutes(apiRouter)
|
||||
metricsHandler.RegisterRoutes(apiRouter)
|
||||
wakatimeV1AllHandler.RegisterRoutes(apiRouter)
|
||||
wakatimeV1SummariesHandler.RegisterRoutes(apiRouter)
|
||||
wakatimeV1StatsHandler.RegisterRoutes(apiRouter)
|
||||
|
@ -2,7 +2,6 @@ package middlewares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/services"
|
||||
@ -15,6 +14,7 @@ type AuthenticateMiddleware struct {
|
||||
config *conf.Config
|
||||
userSrvc services.IUserService
|
||||
optionalForPaths []string
|
||||
redirectTarget string // optional
|
||||
}
|
||||
|
||||
func NewAuthenticateMiddleware(userService services.IUserService) *AuthenticateMiddleware {
|
||||
@ -30,6 +30,11 @@ func (m *AuthenticateMiddleware) WithOptionalFor(paths []string) *AuthenticateMi
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *AuthenticateMiddleware) WithRedirectTarget(path string) *AuthenticateMiddleware {
|
||||
m.redirectTarget = path
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *AuthenticateMiddleware) Handler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
m.ServeHTTP(w, r, h.ServeHTTP)
|
||||
@ -50,11 +55,12 @@ func (m *AuthenticateMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, "/api") {
|
||||
if m.redirectTarget == "" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(conf.ErrUnauthorized))
|
||||
} else {
|
||||
http.SetCookie(w, m.config.GetClearCookie(models.AuthCookieKey, "/"))
|
||||
http.Redirect(w, r, fmt.Sprintf("%s/?error=unauthorized", m.config.Server.BasePath), http.StatusFound)
|
||||
http.Redirect(w, r, m.redirectTarget, http.StatusFound)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -4,23 +4,24 @@ package middlewares
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type logFunc func(string, ...interface{})
|
||||
|
||||
type LoggingMiddleware struct {
|
||||
handler http.Handler
|
||||
output *log.Logger
|
||||
logFunc logFunc
|
||||
excludePrefixes []string
|
||||
}
|
||||
|
||||
func NewLoggingMiddleware(output *log.Logger, excludePrefixes []string) func(http.Handler) http.Handler {
|
||||
func NewLoggingMiddleware(logFunc logFunc, excludePrefixes []string) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
return &LoggingMiddleware{
|
||||
handler: h,
|
||||
output: output,
|
||||
logFunc: logFunc,
|
||||
excludePrefixes: excludePrefixes,
|
||||
}
|
||||
}
|
||||
@ -41,9 +42,8 @@ func (lg *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
lg.output.Printf(
|
||||
"%v status=%d, method=%s, uri=%s, duration=%v, bytes=%d, addr=%s\n",
|
||||
time.Now().Format(time.RFC3339Nano),
|
||||
lg.logFunc(
|
||||
"[request] status=%d, method=%s, uri=%s, duration=%v, bytes=%d, addr=%s, user=%s\n",
|
||||
ww.Status(),
|
||||
r.Method,
|
||||
r.URL.String(),
|
||||
|
@ -14,7 +14,7 @@ func init() {
|
||||
migrator := db.Migrator()
|
||||
|
||||
if !migrator.HasColumn(&models.User{}, "badges_enabled") {
|
||||
// empty database, nothing to migrate
|
||||
// empty database or already migrated, nothing to migrate
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -37,11 +37,15 @@ func init() {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.Db.Dialect == config.SQLDialectSqlite {
|
||||
logbuch.Info("not attempting to drop column 'badges_enabled' on sqlite")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := migrator.DropColumn(&models.User{}, "badges_enabled"); err != nil {
|
||||
return err
|
||||
} else {
|
||||
logbuch.Info("dropped column 'badges_enabled' after substituting it by sharing indicators")
|
||||
}
|
||||
logbuch.Info("dropped column 'badges_enabled' after substituting it by sharing indicators")
|
||||
|
||||
return nil
|
||||
},
|
||||
|
41
migrations/20210213_add_has_data_field.go
Normal file
41
migrations/20210213_add_has_data_field.go
Normal file
@ -0,0 +1,41 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
const name = "20210213-add_has_data_field"
|
||||
f := migrationFunc{
|
||||
name: name,
|
||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||
condition := "key = ?"
|
||||
if cfg.Db.Dialect == config.SQLDialectMysql {
|
||||
condition = "`key` = ?"
|
||||
}
|
||||
lookupResult := db.Where(condition, name).First(&models.KeyStringValue{})
|
||||
if lookupResult.Error == nil && lookupResult.RowsAffected > 0 {
|
||||
logbuch.Info("no need to migrate '%s'", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := db.Exec("UPDATE users SET has_data = TRUE WHERE TRUE").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.Create(&models.KeyStringValue{
|
||||
Key: name,
|
||||
Value: "done",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
registerPostMigration(f)
|
||||
}
|
@ -20,11 +20,21 @@ func (m *HeartbeatServiceMock) InsertBatch(heartbeats []*models.Heartbeat) error
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *HeartbeatServiceMock) Count() (int64, error) {
|
||||
args := m.Called()
|
||||
return int64(args.Int(0)), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *HeartbeatServiceMock) CountByUser(user *models.User) (int64, error) {
|
||||
args := m.Called(user)
|
||||
return args.Get(0).(int64), args.Error(0)
|
||||
}
|
||||
|
||||
func (m *HeartbeatServiceMock) CountByUsers(users []*models.User) ([]*models.CountByUser, error) {
|
||||
args := m.Called(users)
|
||||
return args.Get(0).([]*models.CountByUser), args.Error(0)
|
||||
}
|
||||
|
||||
func (m *HeartbeatServiceMock) GetAllWithin(time time.Time, time2 time.Time, user *models.User) ([]*models.Heartbeat, error) {
|
||||
args := m.Called(time, time2, user)
|
||||
return args.Get(0).([]*models.Heartbeat), args.Error(1)
|
||||
|
@ -24,8 +24,18 @@ func (m *UserServiceMock) GetAll() ([]*models.User, error) {
|
||||
return args.Get(0).([]*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) CreateOrGet(signup *models.Signup) (*models.User, bool, error) {
|
||||
args := m.Called(signup)
|
||||
func (m *UserServiceMock) GetActive() ([]*models.User, error) {
|
||||
args := m.Called()
|
||||
return args.Get(0).([]*models.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) Count() (int64, error) {
|
||||
args := m.Called()
|
||||
return int64(args.Int(0)), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *UserServiceMock) CreateOrGet(signup *models.Signup, isAdmin bool) (*models.User, bool, error) {
|
||||
args := m.Called(signup, isAdmin)
|
||||
return args.Get(0).(*models.User), args.Bool(1), args.Error(2)
|
||||
}
|
||||
|
||||
|
@ -13,9 +13,18 @@ type AllTimeViewModel struct {
|
||||
}
|
||||
|
||||
type AllTimeData struct {
|
||||
TotalSeconds float32 `json:"total_seconds"` // total number of seconds logged since account created
|
||||
Text string `json:"text"` // total time logged since account created as human readable string>
|
||||
IsUpToDate bool `json:"is_up_to_date"` // true if the stats are up to date; when false, a 202 response code is returned and stats will be refreshed soon>
|
||||
TotalSeconds float32 `json:"total_seconds"` // total number of seconds logged since account created
|
||||
Text string `json:"text"` // total time logged since account created as human readable string>
|
||||
IsUpToDate bool `json:"is_up_to_date"` // true if the stats are up to date; when false, a 202 response code is returned and stats will be refreshed soon>
|
||||
Range *AllTimeRange `json:"range"`
|
||||
}
|
||||
|
||||
type AllTimeRange struct {
|
||||
End string `json:"end"`
|
||||
EndDate string `json:"end_date"`
|
||||
Start string `json:"start"`
|
||||
StartDate string `json:"start_date"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
|
||||
func NewAllTimeFrom(summary *models.Summary, filters *models.Filters) *AllTimeViewModel {
|
||||
|
12
models/compat/wakatime/v1/machine.go
Normal file
12
models/compat/wakatime/v1/machine.go
Normal file
@ -0,0 +1,12 @@
|
||||
package v1
|
||||
|
||||
// https://wakatime.com/api/v1/users/current/machine_names
|
||||
|
||||
type MachineViewModel struct {
|
||||
Data []*MachineEntry `json:"data"`
|
||||
}
|
||||
|
||||
type MachineEntry struct {
|
||||
Id string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
}
|
22
models/metrics/counter_metric.go
Normal file
22
models/metrics/counter_metric.go
Normal file
@ -0,0 +1,22 @@
|
||||
package metrics
|
||||
|
||||
import "fmt"
|
||||
|
||||
type CounterMetric struct {
|
||||
Name string
|
||||
Value int
|
||||
Desc string
|
||||
Labels Labels
|
||||
}
|
||||
|
||||
func (c CounterMetric) Key() string {
|
||||
return c.Name
|
||||
}
|
||||
|
||||
func (c CounterMetric) Print() string {
|
||||
return fmt.Sprintf("%s%s %d", c.Name, c.Labels.Print(), c.Value)
|
||||
}
|
||||
|
||||
func (c CounterMetric) Header() string {
|
||||
return fmt.Sprintf("# HELP %s %s\n# TYPE %s counter", c.Name, c.Desc, c.Name)
|
||||
}
|
28
models/metrics/label.go
Normal file
28
models/metrics/label.go
Normal file
@ -0,0 +1,28 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Labels []Label
|
||||
|
||||
type Label struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (l Labels) Print() string {
|
||||
printedLabels := make([]string, len(l))
|
||||
for i, e := range l {
|
||||
printedLabels[i] = e.Print()
|
||||
}
|
||||
if len(l) == 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("{%s}", strings.Join(printedLabels, ","))
|
||||
}
|
||||
|
||||
func (l Label) Print() string {
|
||||
return fmt.Sprintf("%s=\"%s\"", l.Key, l.Value)
|
||||
}
|
43
models/metrics/metric.go
Normal file
43
models/metrics/metric.go
Normal file
@ -0,0 +1,43 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Hand-crafted Prometheus metrics
|
||||
// Since we're only using very simple counters in this application,
|
||||
// we don't actually need the official client SDK as a dependency
|
||||
|
||||
type Metrics []Metric
|
||||
|
||||
func (m Metrics) Print() (output string) {
|
||||
printedMetrics := make(map[string]bool)
|
||||
for _, m := range m {
|
||||
if _, ok := printedMetrics[m.Key()]; !ok {
|
||||
output += fmt.Sprintf("%s\n", m.Header())
|
||||
printedMetrics[m.Key()] = true
|
||||
}
|
||||
output += fmt.Sprintf("%s\n", m.Print())
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func (m Metrics) Len() int {
|
||||
return len(m)
|
||||
}
|
||||
|
||||
func (m Metrics) Less(i, j int) bool {
|
||||
return strings.Compare(m[i].Key(), m[j].Key()) < 0
|
||||
}
|
||||
|
||||
func (m Metrics) Swap(i, j int) {
|
||||
m[i], m[j] = m[j], m[i]
|
||||
}
|
||||
|
||||
type Metric interface {
|
||||
Key() string
|
||||
Header() string
|
||||
Print() string
|
||||
}
|
@ -47,12 +47,14 @@ type SummaryItemContainer struct {
|
||||
|
||||
type SummaryViewModel struct {
|
||||
*Summary
|
||||
User *User
|
||||
LanguageColors map[string]string
|
||||
EditorColors map[string]string
|
||||
OSColors map[string]string
|
||||
Error string
|
||||
Success string
|
||||
ApiKey string
|
||||
RawQuery string
|
||||
}
|
||||
|
||||
type SummaryParams struct {
|
||||
@ -91,6 +93,10 @@ func (s *Summary) MappedItems() map[uint8]*SummaryItems {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Summary) ItemsByType(summaryType uint8) *SummaryItems {
|
||||
return s.MappedItems()[summaryType]
|
||||
}
|
||||
|
||||
/* Augments the summary in a way that at least one item is present for every type.
|
||||
If a summary has zero items for a given type, but one or more for any of the other types,
|
||||
the total summary duration can be derived from those and inserted as a dummy-item with key "unknown"
|
||||
|
@ -12,6 +12,8 @@ type User struct {
|
||||
ShareProjects bool `json:"-" gorm:"default:false; type:bool"`
|
||||
ShareOSs bool `json:"-" gorm:"default:false; type:bool; column:share_oss"`
|
||||
ShareMachines bool `json:"-" gorm:"default:false; type:bool"`
|
||||
IsAdmin bool `json:"-" gorm:"default:false; type:bool"`
|
||||
HasData bool `json:"-" gorm:"default:false; type:bool"`
|
||||
WakatimeApiKey string `json:"-"`
|
||||
}
|
||||
|
||||
@ -37,6 +39,11 @@ type TimeByUser struct {
|
||||
Time CustomTime
|
||||
}
|
||||
|
||||
type CountByUser struct {
|
||||
User string
|
||||
Count int64
|
||||
}
|
||||
|
||||
func (c *CredentialsReset) IsValid() bool {
|
||||
return validatePassword(c.PasswordNew) &&
|
||||
c.PasswordNew == c.PasswordRepeat
|
||||
|
@ -1,8 +1,9 @@
|
||||
package view
|
||||
|
||||
type LoginViewModel struct {
|
||||
Success string
|
||||
Error string
|
||||
Success string
|
||||
Error string
|
||||
TotalUsers int
|
||||
}
|
||||
|
||||
func (s *LoginViewModel) WithSuccess(m string) *LoginViewModel {
|
||||
|
@ -26,17 +26,6 @@ func (r *HeartbeatRepository) InsertBatch(heartbeats []*models.Heartbeat) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *HeartbeatRepository) CountByUser(user *models.User) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.
|
||||
Model(&models.Heartbeat{}).
|
||||
Where(&models.Heartbeat{UserID: user.ID}).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *HeartbeatRepository) GetLatestByOriginAndUser(origin string, user *models.User) (*models.Heartbeat, error) {
|
||||
var heartbeat models.Heartbeat
|
||||
if err := r.db.
|
||||
@ -75,6 +64,57 @@ func (r *HeartbeatRepository) GetFirstByUsers() ([]*models.TimeByUser, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *HeartbeatRepository) GetLastByUsers() ([]*models.TimeByUser, error) {
|
||||
var result []*models.TimeByUser
|
||||
r.db.Model(&models.User{}).
|
||||
Select("users.id as user, max(time) as time").
|
||||
Joins("left join heartbeats on users.id = heartbeats.user_id").
|
||||
Group("user").
|
||||
Scan(&result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *HeartbeatRepository) Count() (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.
|
||||
Model(&models.Heartbeat{}).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *HeartbeatRepository) CountByUser(user *models.User) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.
|
||||
Model(&models.Heartbeat{}).
|
||||
Where(&models.Heartbeat{UserID: user.ID}).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *HeartbeatRepository) CountByUsers(users []*models.User) ([]*models.CountByUser, error) {
|
||||
var counts []*models.CountByUser
|
||||
|
||||
userIds := make([]string, len(users))
|
||||
for i, u := range users {
|
||||
userIds[i] = u.ID
|
||||
}
|
||||
|
||||
if err := r.db.
|
||||
Model(&models.User{}).
|
||||
Select("users.id as user, count(heartbeats.id) as count").
|
||||
Joins("left join heartbeats on users.id = heartbeats.user_id").
|
||||
Where("user_id in ?", userIds).
|
||||
Group("user").
|
||||
Find(&counts).Error; err != nil {
|
||||
return counts, err
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (r *HeartbeatRepository) DeleteBefore(t time.Time) error {
|
||||
if err := r.db.
|
||||
Where("time <= ?", t).
|
||||
|
@ -17,10 +17,13 @@ type IAliasRepository interface {
|
||||
|
||||
type IHeartbeatRepository interface {
|
||||
InsertBatch([]*models.Heartbeat) error
|
||||
CountByUser(*models.User) (int64, error)
|
||||
GetAllWithin(time.Time, time.Time, *models.User) ([]*models.Heartbeat, error)
|
||||
GetFirstByUsers() ([]*models.TimeByUser, error)
|
||||
GetLastByUsers() ([]*models.TimeByUser, error)
|
||||
GetLatestByOriginAndUser(string, *models.User) (*models.Heartbeat, error)
|
||||
Count() (int64, error)
|
||||
CountByUser(*models.User) (int64, error)
|
||||
CountByUsers([]*models.User) ([]*models.CountByUser, error)
|
||||
DeleteBefore(time.Time) error
|
||||
}
|
||||
|
||||
@ -46,8 +49,12 @@ type ISummaryRepository interface {
|
||||
|
||||
type IUserRepository interface {
|
||||
GetById(string) (*models.User, error)
|
||||
GetByIds([]string) ([]*models.User, error)
|
||||
GetByApiKey(string) (*models.User, error)
|
||||
GetAll() ([]*models.User, error)
|
||||
GetByLoggedInAfter(time.Time) ([]*models.User, error)
|
||||
GetByLastActiveAfter(time.Time) ([]*models.User, error)
|
||||
Count() (int64, error)
|
||||
InsertOrGet(*models.User) (*models.User, bool, error)
|
||||
Update(*models.User) (*models.User, error)
|
||||
UpdateField(*models.User, string, interface{}) (*models.User, error)
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"github.com/muety/wakapi/models"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
@ -22,6 +23,17 @@ func (r *UserRepository) GetById(userId string) (*models.User, error) {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByIds(userIds []string) ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := r.db.
|
||||
Model(&models.User{}).
|
||||
Where("id in ?", userIds).
|
||||
Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByApiKey(key string) (*models.User, error) {
|
||||
u := &models.User{}
|
||||
if err := r.db.Where(&models.User{ApiKey: key}).First(u).Error; err != nil {
|
||||
@ -40,6 +52,46 @@ func (r *UserRepository) GetAll() ([]*models.User, error) {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) GetByLoggedInAfter(t time.Time) ([]*models.User, error) {
|
||||
var users []*models.User
|
||||
if err := r.db.
|
||||
Where("last_logged_in_at >= ?", t).
|
||||
Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// Returns a list of user ids, whose last heartbeat is not older than t
|
||||
// NOTE: Only ID field will be populated
|
||||
func (r *UserRepository) GetByLastActiveAfter(t time.Time) ([]*models.User, error) {
|
||||
subQuery1 := r.db.Model(&models.User{}).
|
||||
Select("users.id as user, max(time) as time").
|
||||
Joins("left join heartbeats on users.id = heartbeats.user_id").
|
||||
Group("user")
|
||||
|
||||
var userIds []string
|
||||
if err := r.db.
|
||||
Select("user as id").
|
||||
Table("(?) as q", subQuery1).
|
||||
Where("time >= ?", t).
|
||||
Scan(&userIds).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.GetByIds(userIds)
|
||||
}
|
||||
|
||||
func (r *UserRepository) Count() (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.
|
||||
Model(&models.User{}).
|
||||
Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) InsertOrGet(user *models.User) (*models.User, bool, error) {
|
||||
result := r.db.FirstOrCreate(user, &models.User{ID: user.ID})
|
||||
if err := result.Error; err != nil {
|
||||
@ -65,6 +117,7 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
|
||||
"share_projects": user.ShareProjects,
|
||||
"share_machines": user.ShareMachines,
|
||||
"wakatime_api_key": user.WakatimeApiKey,
|
||||
"has_data": user.HasData,
|
||||
}
|
||||
|
||||
result := r.db.Model(user).Updates(updateMap)
|
||||
|
@ -73,7 +73,7 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if !hb.Valid() {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Invalid heartbeat object."))
|
||||
w.Write([]byte("invalid heartbeat object"))
|
||||
return
|
||||
}
|
||||
|
||||
@ -82,10 +82,21 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := h.heartbeatSrvc.InsertBatch(heartbeats); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
logbuch.Error(err.Error())
|
||||
w.Write([]byte(conf.ErrInternalServerError))
|
||||
logbuch.Error("failed to batch-insert heartbeats – %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !user.HasData {
|
||||
user.HasData = true
|
||||
if _, err := h.userSrvc.Update(user); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(conf.ErrInternalServerError))
|
||||
logbuch.Error("failed to update user – %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
utils.RespondJSON(w, http.StatusCreated, constructSuccessResponse(len(heartbeats)))
|
||||
}
|
||||
|
||||
|
271
routes/api/metrics.go
Normal file
271
routes/api/metrics.go
Normal file
@ -0,0 +1,271 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/gorilla/mux"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"github.com/muety/wakapi/models"
|
||||
v1 "github.com/muety/wakapi/models/compat/wakatime/v1"
|
||||
mm "github.com/muety/wakapi/models/metrics"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
MetricsPrefix = "wakatime"
|
||||
|
||||
DescHeartbeats = "Total number of tracked heartbeats."
|
||||
DescAllTime = "Total seconds (all time)."
|
||||
DescTotal = "Total seconds."
|
||||
DescEditors = "Total seconds for each editor."
|
||||
DescProjects = "Total seconds for each project."
|
||||
DescLanguages = "Total seconds for each language."
|
||||
DescOperatingSystems = "Total seconds for each operating system."
|
||||
DescMachines = "Total seconds for each machine."
|
||||
|
||||
DescAdminTotalTime = "Total seconds (all users, all time)."
|
||||
DescAdminTotalHeartbeats = "Total number of tracked heartbeats (all users, all time)"
|
||||
DescAdminUserHeartbeats = "Total number of tracked heartbeats by user (all time)."
|
||||
DescAdminTotalUsers = "Total number of registered users."
|
||||
DescAdminActiveUsers = "Number of active users."
|
||||
)
|
||||
|
||||
type MetricsHandler struct {
|
||||
config *conf.Config
|
||||
userSrvc services.IUserService
|
||||
summarySrvc services.ISummaryService
|
||||
heartbeatSrvc services.IHeartbeatService
|
||||
keyValueSrvc services.IKeyValueService
|
||||
}
|
||||
|
||||
func NewMetricsHandler(userService services.IUserService, summaryService services.ISummaryService, heartbeatService services.IHeartbeatService, keyValueService services.IKeyValueService) *MetricsHandler {
|
||||
return &MetricsHandler{
|
||||
userSrvc: userService,
|
||||
summarySrvc: summaryService,
|
||||
heartbeatSrvc: heartbeatService,
|
||||
keyValueSrvc: keyValueService,
|
||||
config: conf.Get(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MetricsHandler) RegisterRoutes(router *mux.Router) {
|
||||
if !h.config.Security.ExposeMetrics {
|
||||
return
|
||||
}
|
||||
|
||||
logbuch.Info("exposing prometheus metrics under /api/metrics")
|
||||
|
||||
r := router.PathPrefix("/metrics").Subrouter()
|
||||
r.Use(
|
||||
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
|
||||
)
|
||||
r.Methods(http.MethodGet).HandlerFunc(h.Get)
|
||||
}
|
||||
|
||||
func (h *MetricsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
reqUser := r.Context().Value(models.UserKey).(*models.User)
|
||||
if reqUser == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(conf.ErrUnauthorized))
|
||||
return
|
||||
}
|
||||
|
||||
var metrics mm.Metrics
|
||||
|
||||
if userMetrics, err := h.getUserMetrics(reqUser); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(conf.ErrInternalServerError))
|
||||
return
|
||||
} else {
|
||||
for _, m := range *userMetrics {
|
||||
metrics = append(metrics, m)
|
||||
}
|
||||
}
|
||||
|
||||
if reqUser.IsAdmin {
|
||||
if adminMetrics, err := h.getAdminMetrics(reqUser); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(conf.ErrInternalServerError))
|
||||
return
|
||||
} else {
|
||||
for _, m := range *adminMetrics {
|
||||
metrics = append(metrics, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(metrics)
|
||||
|
||||
w.Header().Set("content-type", "text/plain; charset=utf-8")
|
||||
w.Write([]byte(metrics.Print()))
|
||||
}
|
||||
|
||||
func (h *MetricsHandler) getUserMetrics(user *models.User) (*mm.Metrics, error) {
|
||||
var metrics mm.Metrics
|
||||
|
||||
summaryAllTime, err := h.summarySrvc.Aliased(time.Time{}, time.Now(), user, h.summarySrvc.Retrieve)
|
||||
if err != nil {
|
||||
logbuch.Error("failed to retrieve all time summary for user '%s' for metric", user.ID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
from, to := utils.MustResolveIntervalRaw("today")
|
||||
|
||||
summaryToday, err := h.summarySrvc.Aliased(from, to, user, h.summarySrvc.Retrieve)
|
||||
if err != nil {
|
||||
logbuch.Error("failed to retrieve today's summary for user '%s' for metric", user.ID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
heartbeatCount, err := h.heartbeatSrvc.CountByUser(user)
|
||||
if err != nil {
|
||||
logbuch.Error("failed to count heartbeats for user '%s' for metric", user.ID)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// User Metrics
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_cumulative_seconds_total",
|
||||
Desc: DescAllTime,
|
||||
Value: int(v1.NewAllTimeFrom(summaryAllTime, &models.Filters{}).Data.TotalSeconds),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_seconds_total",
|
||||
Desc: DescTotal,
|
||||
Value: int(summaryToday.TotalTime().Seconds()),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_heartbeats_total",
|
||||
Desc: DescHeartbeats,
|
||||
Value: int(heartbeatCount),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
for _, p := range summaryToday.Projects {
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_project_seconds_total",
|
||||
Desc: DescProjects,
|
||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryProject, p.Key).Seconds()),
|
||||
Labels: []mm.Label{{Key: "name", Value: p.Key}},
|
||||
})
|
||||
}
|
||||
|
||||
for _, l := range summaryToday.Languages {
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_language_seconds_total",
|
||||
Desc: DescLanguages,
|
||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryLanguage, l.Key).Seconds()),
|
||||
Labels: []mm.Label{{Key: "name", Value: l.Key}},
|
||||
})
|
||||
}
|
||||
|
||||
for _, e := range summaryToday.Editors {
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_editor_seconds_total",
|
||||
Desc: DescEditors,
|
||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryEditor, e.Key).Seconds()),
|
||||
Labels: []mm.Label{{Key: "name", Value: e.Key}},
|
||||
})
|
||||
}
|
||||
|
||||
for _, o := range summaryToday.OperatingSystems {
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_operating_system_seconds_total",
|
||||
Desc: DescOperatingSystems,
|
||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryOS, o.Key).Seconds()),
|
||||
Labels: []mm.Label{{Key: "name", Value: o.Key}},
|
||||
})
|
||||
}
|
||||
|
||||
for _, m := range summaryToday.Machines {
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_machine_seconds_total",
|
||||
Desc: DescMachines,
|
||||
Value: int(summaryToday.TotalTimeByKey(models.SummaryMachine, m.Key).Seconds()),
|
||||
Labels: []mm.Label{{Key: "name", Value: m.Key}},
|
||||
})
|
||||
}
|
||||
|
||||
return &metrics, nil
|
||||
}
|
||||
|
||||
func (h *MetricsHandler) getAdminMetrics(user *models.User) (*mm.Metrics, error) {
|
||||
var metrics mm.Metrics
|
||||
|
||||
if !user.IsAdmin {
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
var totalSeconds int
|
||||
if t, err := h.keyValueSrvc.GetString(conf.KeyLatestTotalTime); err == nil && t != nil && t.Value != "" {
|
||||
if d, err := time.ParseDuration(t.Value); err == nil {
|
||||
totalSeconds = int(d.Seconds())
|
||||
}
|
||||
}
|
||||
|
||||
totalUsers, _ := h.userSrvc.Count()
|
||||
totalHeartbeats, _ := h.heartbeatSrvc.Count()
|
||||
|
||||
activeUsers, err := h.userSrvc.GetActive()
|
||||
if err != nil {
|
||||
logbuch.Error("failed to retrieve active users for metric – %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_admin_seconds_total",
|
||||
Desc: DescAdminTotalTime,
|
||||
Value: totalSeconds,
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_admin_heartbeats_total",
|
||||
Desc: DescAdminTotalHeartbeats,
|
||||
Value: int(totalHeartbeats),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_admin_users_total",
|
||||
Desc: DescAdminTotalUsers,
|
||||
Value: int(totalUsers),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_admin_users_active_total",
|
||||
Desc: DescAdminActiveUsers,
|
||||
Value: len(activeUsers),
|
||||
Labels: []mm.Label{},
|
||||
})
|
||||
|
||||
// Count per-user heartbeats
|
||||
|
||||
userCounts, err := h.heartbeatSrvc.CountByUsers(activeUsers)
|
||||
if err != nil {
|
||||
logbuch.Error("failed to count heartbeats for active users", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, uc := range userCounts {
|
||||
metrics = append(metrics, &mm.CounterMetric{
|
||||
Name: MetricsPrefix + "_admin_user_heartbeats_total",
|
||||
Desc: DescAdminUserHeartbeats,
|
||||
Value: int(uc.Count),
|
||||
Labels: []mm.Label{{Key: "user", Value: uc.User}},
|
||||
})
|
||||
}
|
||||
|
||||
return &metrics, nil
|
||||
}
|
@ -33,6 +33,10 @@ func (h *StatsHandler) RegisterRoutes(router *mux.Router) {
|
||||
)
|
||||
r.Path("/v1/users/{user}/stats/{range}").Methods(http.MethodGet).HandlerFunc(h.Get)
|
||||
r.Path("/compat/wakatime/v1/users/{user}/stats/{range}").Methods(http.MethodGet).HandlerFunc(h.Get)
|
||||
|
||||
// Also works without range, see https://github.com/anuraghazra/github-readme-stats/issues/865#issuecomment-776186592
|
||||
r.Path("/v1/users/{user}/stats").Methods(http.MethodGet).HandlerFunc(h.Get)
|
||||
r.Path("/compat/wakatime/v1/users/{user}/stats").Methods(http.MethodGet).HandlerFunc(h.Get)
|
||||
}
|
||||
|
||||
// TODO: support filtering (requires https://github.com/muety/wakapi/issues/108)
|
||||
@ -56,7 +60,12 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err, rangeFrom, rangeTo := utils.ResolveIntervalRaw(vars["range"])
|
||||
rangeParam := vars["range"]
|
||||
if rangeParam == "" {
|
||||
rangeParam = (*models.IntervalPast7Days)[0]
|
||||
}
|
||||
|
||||
err, rangeFrom, rangeTo := utils.ResolveIntervalRaw(rangeParam)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("invalid range"))
|
||||
|
@ -150,7 +150,9 @@ func (h *LoginHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_, created, err := h.userSrvc.CreateOrGet(&signup)
|
||||
numUsers, _ := h.userSrvc.Count()
|
||||
|
||||
_, created, err := h.userSrvc.CreateOrGet(&signup, numUsers == 0)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("failed to create new user"))
|
||||
@ -166,8 +168,11 @@ func (h *LoginHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *LoginHandler) buildViewModel(r *http.Request) *view.LoginViewModel {
|
||||
numUsers, _ := h.userSrvc.Count()
|
||||
|
||||
return &view.LoginViewModel{
|
||||
Success: r.URL.Query().Get("success"),
|
||||
Error: r.URL.Query().Get("error"),
|
||||
Success: r.URL.Query().Get("success"),
|
||||
Error: r.URL.Query().Get("error"),
|
||||
TotalUsers: int(numUsers),
|
||||
}
|
||||
}
|
||||
|
@ -24,15 +24,17 @@ var templates map[string]*template.Template
|
||||
func loadTemplates() {
|
||||
const tplPath = "/views"
|
||||
tpls := template.New("").Funcs(template.FuncMap{
|
||||
"json": utils.Json,
|
||||
"date": utils.FormatDateHuman,
|
||||
"title": strings.Title,
|
||||
"join": strings.Join,
|
||||
"add": utils.Add,
|
||||
"capitalize": utils.Capitalize,
|
||||
"toRunes": utils.ToRunes,
|
||||
"entityTypes": models.SummaryTypes,
|
||||
"typeName": typeName,
|
||||
"json": utils.Json,
|
||||
"date": utils.FormatDateHuman,
|
||||
"simpledate": utils.FormatDate,
|
||||
"simpledatetime": utils.FormatDateTime,
|
||||
"title": strings.Title,
|
||||
"join": strings.Join,
|
||||
"add": utils.Add,
|
||||
"capitalize": utils.Capitalize,
|
||||
"toRunes": utils.ToRunes,
|
||||
"entityTypes": models.SummaryTypes,
|
||||
"typeName": typeName,
|
||||
"getBasePath": func() string {
|
||||
return config.Get().Server.BasePath
|
||||
},
|
||||
@ -102,3 +104,7 @@ func typeName(t uint8) string {
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func defaultErrorRedirectTarget() string {
|
||||
return fmt.Sprintf("%s/?error=unauthorized", config.Get().Server.BasePath)
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ func NewSettingsHandler(
|
||||
func (h *SettingsHandler) RegisterRoutes(router *mux.Router) {
|
||||
r := router.PathPrefix("/settings").Subrouter()
|
||||
r.Use(
|
||||
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
|
||||
middlewares.NewAuthenticateMiddleware(h.userSrvc).WithRedirectTarget(defaultErrorRedirectTarget()).Handler,
|
||||
)
|
||||
r.Methods(http.MethodGet).HandlerFunc(h.GetIndex)
|
||||
r.Methods(http.MethodPost).HandlerFunc(h.PostIndex)
|
||||
@ -166,13 +166,13 @@ func (h *SettingsHandler) actionChangePassword(w http.ResponseWriter, r *http.Re
|
||||
|
||||
user.Password = credentials.PasswordNew
|
||||
if hash, err := utils.HashBcrypt(user.Password, h.config.Security.PasswordSalt); err != nil {
|
||||
return http.StatusInternalServerError, "", "internal server error"
|
||||
return http.StatusInternalServerError, "", conf.ErrInternalServerError
|
||||
} else {
|
||||
user.Password = hash
|
||||
}
|
||||
|
||||
if _, err := h.userSrvc.Update(user); err != nil {
|
||||
return http.StatusInternalServerError, "", "internal server error"
|
||||
return http.StatusInternalServerError, "", conf.ErrInternalServerError
|
||||
}
|
||||
|
||||
login := &models.Login{
|
||||
@ -181,7 +181,7 @@ func (h *SettingsHandler) actionChangePassword(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
encoded, err := h.config.Security.SecureCookie.Encode(models.AuthCookieKey, login.Username)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, "", "internal server error"
|
||||
return http.StatusInternalServerError, "", conf.ErrInternalServerError
|
||||
}
|
||||
|
||||
http.SetCookie(w, h.config.CreateCookie(models.AuthCookieKey, encoded, "/"))
|
||||
@ -195,7 +195,7 @@ func (h *SettingsHandler) actionResetApiKey(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
if _, err := h.userSrvc.ResetApiKey(user); err != nil {
|
||||
return http.StatusInternalServerError, "", "internal server error"
|
||||
return http.StatusInternalServerError, "", conf.ErrInternalServerError
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("your new api key is: %s", user.ApiKey)
|
||||
@ -341,7 +341,7 @@ func (h *SettingsHandler) actionSetWakatimeApiKey(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
if _, err := h.userSrvc.SetWakatimeApiKey(user, apiKey); err != nil {
|
||||
return http.StatusInternalServerError, "", "internal server error"
|
||||
return http.StatusInternalServerError, "", conf.ErrInternalServerError
|
||||
}
|
||||
|
||||
return http.StatusOK, "Wakatime API Key updated successfully", ""
|
||||
|
@ -29,7 +29,7 @@ func NewSummaryHandler(summaryService services.ISummaryService, userService serv
|
||||
func (h *SummaryHandler) RegisterRoutes(router *mux.Router) {
|
||||
r := router.PathPrefix("/summary").Subrouter()
|
||||
r.Use(
|
||||
middlewares.NewAuthenticateMiddleware(h.userSrvc).Handler,
|
||||
middlewares.NewAuthenticateMiddleware(h.userSrvc).WithRedirectTarget(defaultErrorRedirectTarget()).Handler,
|
||||
)
|
||||
r.Methods(http.MethodGet).HandlerFunc(h.GetIndex)
|
||||
}
|
||||
@ -39,6 +39,7 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
rawQuery := r.URL.RawQuery
|
||||
q := r.URL.Query()
|
||||
if q.Get("interval") == "" && q.Get("from") == "" {
|
||||
q.Set("interval", "today")
|
||||
@ -61,10 +62,12 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
vm := models.SummaryViewModel{
|
||||
Summary: summary,
|
||||
User: user,
|
||||
LanguageColors: utils.FilterColors(h.config.App.GetLanguageColors(), summary.Languages),
|
||||
EditorColors: utils.FilterColors(h.config.App.GetEditorColors(), summary.Editors),
|
||||
OSColors: utils.FilterColors(h.config.App.GetOSColors(), summary.OperatingSystems),
|
||||
ApiKey: user.ApiKey,
|
||||
RawQuery: rawQuery,
|
||||
}
|
||||
|
||||
templates[conf.SummaryTemplate].Execute(w, vm)
|
||||
|
@ -30,10 +30,18 @@ func (srv *HeartbeatService) InsertBatch(heartbeats []*models.Heartbeat) error {
|
||||
return srv.repository.InsertBatch(heartbeats)
|
||||
}
|
||||
|
||||
func (srv *HeartbeatService) Count() (int64, error) {
|
||||
return srv.repository.Count()
|
||||
}
|
||||
|
||||
func (srv *HeartbeatService) CountByUser(user *models.User) (int64, error) {
|
||||
return srv.repository.CountByUser(user)
|
||||
}
|
||||
|
||||
func (srv *HeartbeatService) CountByUsers(users []*models.User) ([]*models.CountByUser, error) {
|
||||
return srv.repository.CountByUsers(users)
|
||||
}
|
||||
|
||||
func (srv *HeartbeatService) GetAllWithin(from, to time.Time, user *models.User) ([]*models.Heartbeat, error) {
|
||||
heartbeats, err := srv.repository.GetAllWithin(from, to, user)
|
||||
if err != nil {
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/wakapi/config"
|
||||
@ -53,6 +52,12 @@ func (w *WakatimeHeartbeatImporter) Import(user *models.User, minFrom time.Time,
|
||||
return
|
||||
}
|
||||
|
||||
machinesNames, err := w.fetchMachineNames()
|
||||
if err != nil {
|
||||
logbuch.Error("failed to fetch machine names while importing wakatime heartbeats for user '%s' – %v", user.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
days := generateDays(startDate, endDate)
|
||||
|
||||
c := atomic.NewUint32(uint32(len(days)))
|
||||
@ -68,14 +73,14 @@ func (w *WakatimeHeartbeatImporter) Import(user *models.User, minFrom time.Time,
|
||||
go func(day time.Time) {
|
||||
defer sem.Release(1)
|
||||
|
||||
d := day.Format("2006-01-02")
|
||||
d := day.Format(config.SimpleDateFormat)
|
||||
heartbeats, err := w.fetchHeartbeats(d)
|
||||
if err != nil {
|
||||
logbuch.Error("failed to fetch heartbeats for day '%s' and user '%s' – &v", day, user.ID, err)
|
||||
}
|
||||
|
||||
for _, h := range heartbeats {
|
||||
out <- mapHeartbeat(h, userAgents, user)
|
||||
out <- mapHeartbeat(h, userAgents, machinesNames, user)
|
||||
}
|
||||
|
||||
if c.Dec() == 0 {
|
||||
@ -134,27 +139,17 @@ func (w *WakatimeHeartbeatImporter) fetchRange() (time.Time, time.Time, error) {
|
||||
return notime, notime, err
|
||||
}
|
||||
|
||||
var allTimeData map[string]interface{}
|
||||
var allTimeData wakatime.AllTimeViewModel
|
||||
if err := json.NewDecoder(res.Body).Decode(&allTimeData); err != nil {
|
||||
return notime, notime, err
|
||||
}
|
||||
|
||||
data := allTimeData["data"].(map[string]interface{})
|
||||
if data == nil {
|
||||
return notime, notime, errors.New("invalid response")
|
||||
}
|
||||
|
||||
dataRange := data["range"].(map[string]interface{})
|
||||
if dataRange == nil {
|
||||
return notime, notime, errors.New("invalid response")
|
||||
}
|
||||
|
||||
startDate, err := time.Parse("2006-01-02", dataRange["start_date"].(string))
|
||||
startDate, err := time.Parse(config.SimpleDateFormat, allTimeData.Data.Range.StartDate)
|
||||
if err != nil {
|
||||
return notime, notime, err
|
||||
}
|
||||
|
||||
endDate, err := time.Parse("2006-01-02", dataRange["end_date"].(string))
|
||||
endDate, err := time.Parse(config.SimpleDateFormat, allTimeData.Data.Range.EndDate)
|
||||
if err != nil {
|
||||
return notime, notime, err
|
||||
}
|
||||
@ -189,6 +184,33 @@ func (w *WakatimeHeartbeatImporter) fetchUserAgents() (map[string]*wakatime.User
|
||||
return userAgents, nil
|
||||
}
|
||||
|
||||
// https://wakatime.com/api/v1/users/current/machine_names
|
||||
func (w *WakatimeHeartbeatImporter) fetchMachineNames() (map[string]*wakatime.MachineEntry, error) {
|
||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, config.WakatimeApiUrl+config.WakatimeApiMachineNamesUrl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := httpClient.Do(w.withHeaders(req))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var machineData wakatime.MachineViewModel
|
||||
if err := json.NewDecoder(res.Body).Decode(&machineData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
machines := make(map[string]*wakatime.MachineEntry)
|
||||
for _, ma := range machineData.Data {
|
||||
machines[ma.Id] = ma
|
||||
}
|
||||
|
||||
return machines, nil
|
||||
}
|
||||
|
||||
func (w *WakatimeHeartbeatImporter) withHeaders(req *http.Request) *http.Request {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(w.ApiKey))))
|
||||
return req
|
||||
@ -197,6 +219,7 @@ func (w *WakatimeHeartbeatImporter) withHeaders(req *http.Request) *http.Request
|
||||
func mapHeartbeat(
|
||||
entry *wakatime.HeartbeatEntry,
|
||||
userAgents map[string]*wakatime.UserAgentEntry,
|
||||
machineNames map[string]*wakatime.MachineEntry,
|
||||
user *models.User,
|
||||
) *models.Heartbeat {
|
||||
ua := userAgents[entry.UserAgentId]
|
||||
@ -207,6 +230,14 @@ func mapHeartbeat(
|
||||
}
|
||||
}
|
||||
|
||||
ma := machineNames[entry.MachineNameId]
|
||||
if ma == nil {
|
||||
ma = &wakatime.MachineEntry{
|
||||
Id: entry.MachineNameId,
|
||||
Value: entry.MachineNameId,
|
||||
}
|
||||
}
|
||||
|
||||
return (&models.Heartbeat{
|
||||
User: user,
|
||||
UserID: user.ID,
|
||||
@ -219,7 +250,7 @@ func mapHeartbeat(
|
||||
IsWrite: entry.IsWrite,
|
||||
Editor: ua.Editor,
|
||||
OperatingSystem: ua.Os,
|
||||
Machine: entry.MachineNameId, // TODO
|
||||
Machine: ma.Value,
|
||||
Time: entry.Time,
|
||||
Origin: OriginWakatime,
|
||||
OriginId: entry.Id,
|
||||
|
@ -46,7 +46,7 @@ func (srv *MiscService) ScheduleCountTotalTime() {
|
||||
}
|
||||
|
||||
s := gocron.NewScheduler(time.Local)
|
||||
s.Every(1).Day().At(srv.config.App.CountingTime).Do(srv.runCountTotalTime)
|
||||
s.Every(1).Hour().Do(srv.runCountTotalTime)
|
||||
s.StartBlocking()
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,9 @@ type IAliasService interface {
|
||||
type IHeartbeatService interface {
|
||||
Insert(*models.Heartbeat) error
|
||||
InsertBatch([]*models.Heartbeat) error
|
||||
Count() (int64, error)
|
||||
CountByUser(*models.User) (int64, error)
|
||||
CountByUsers([]*models.User) ([]*models.CountByUser, error)
|
||||
GetAllWithin(time.Time, time.Time, *models.User) ([]*models.Heartbeat, error)
|
||||
GetFirstByUsers() ([]*models.TimeByUser, error)
|
||||
GetLatestByOriginAndUser(string, *models.User) (*models.Heartbeat, error)
|
||||
@ -63,7 +65,9 @@ type IUserService interface {
|
||||
GetUserById(string) (*models.User, error)
|
||||
GetUserByKey(string) (*models.User, error)
|
||||
GetAll() ([]*models.User, error)
|
||||
CreateOrGet(*models.Signup) (*models.User, bool, error)
|
||||
GetActive() ([]*models.User, error)
|
||||
Count() (int64, error)
|
||||
CreateOrGet(*models.Signup, bool) (*models.User, bool, error)
|
||||
Update(*models.User) (*models.User, error)
|
||||
Delete(*models.User) error
|
||||
ResetApiKey(*models.User) (*models.User, error)
|
||||
|
@ -56,11 +56,21 @@ func (srv *UserService) GetAll() ([]*models.User, error) {
|
||||
return srv.repository.GetAll()
|
||||
}
|
||||
|
||||
func (srv *UserService) CreateOrGet(signup *models.Signup) (*models.User, bool, error) {
|
||||
func (srv *UserService) GetActive() ([]*models.User, error) {
|
||||
minDate := time.Now().Add(-24 * time.Hour * time.Duration(srv.Config.App.InactiveDays))
|
||||
return srv.repository.GetByLastActiveAfter(minDate)
|
||||
}
|
||||
|
||||
func (srv *UserService) Count() (int64, error) {
|
||||
return srv.repository.Count()
|
||||
}
|
||||
|
||||
func (srv *UserService) CreateOrGet(signup *models.Signup, isAdmin bool) (*models.User, bool, error) {
|
||||
u := &models.User{
|
||||
ID: signup.Username,
|
||||
ApiKey: uuid.NewV4().String(),
|
||||
Password: signup.Password,
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
if hash, err := utils.HashBcrypt(u.Password, srv.Config.Security.PasswordSalt); err != nil {
|
||||
|
@ -4,4 +4,9 @@ body {
|
||||
|
||||
.bg-gray-850 {
|
||||
background-color: #242b3a;
|
||||
}
|
||||
|
||||
::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
cursor: pointer;
|
||||
}
|
@ -49,6 +49,13 @@ String.prototype.toHHMMSS = function () {
|
||||
return hours + ':' + minutes + ':' + seconds
|
||||
}
|
||||
|
||||
String.prototype.toHHMM = function () {
|
||||
const sec_num = parseInt(this, 10)
|
||||
const hours = Math.floor(sec_num / 3600)
|
||||
const minutes = Math.floor((sec_num - (hours * 3600)) / 60)
|
||||
return hours + ':' + minutes
|
||||
}
|
||||
|
||||
function draw(subselection) {
|
||||
function getTooltipOptions(key) {
|
||||
return {
|
||||
@ -317,8 +324,11 @@ function equalizeHeights() {
|
||||
}
|
||||
|
||||
function getTotal(items) {
|
||||
let total = items.reduce((acc, d) => acc + d.total, 0)
|
||||
document.getElementById('total-span').innerText = total.toString().toHHMMSS()
|
||||
const el = document.getElementById('total-span')
|
||||
if (!el) return
|
||||
const total = items.reduce((acc, d) => acc + d.total, 0)
|
||||
const formatted = total.toString().toHHMM()
|
||||
el.innerText = `${formatted.split(':')[0]} hours, ${formatted.split(':')[1]} minutes`
|
||||
}
|
||||
|
||||
function getRandomColor(seed) {
|
||||
|
@ -7,15 +7,15 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="271.70874mm"
|
||||
width="275.9418mm"
|
||||
height="107.06042mm"
|
||||
viewBox="0 0 271.70874 107.06042"
|
||||
viewBox="0 0 275.9418 107.06042"
|
||||
version="1.1"
|
||||
id="svg4151"
|
||||
id="svg1621"
|
||||
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
||||
sodipodi:docname="logo-text-only.svg">
|
||||
<defs
|
||||
id="defs4145">
|
||||
id="defs1615">
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath20">
|
||||
@ -32,19 +32,19 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="367.75106"
|
||||
inkscape:cy="930.89032"
|
||||
inkscape:cx="-275.67803"
|
||||
inkscape:cy="605.17604"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1346"
|
||||
inkscape:window-height="1198"
|
||||
inkscape:window-x="99"
|
||||
inkscape:window-y="63"
|
||||
inkscape:window-x="149"
|
||||
inkscape:window-y="113"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata4148">
|
||||
id="metadata1618">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
@ -59,10 +59,10 @@
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-8.5325331,98.131398)">
|
||||
transform="translate(-178.77314,11.952827)">
|
||||
<g
|
||||
id="g14"
|
||||
transform="matrix(0.35277777,0,0,-0.35277777,-65.229351,79.309389)">
|
||||
transform="matrix(0.35277777,0,0,-0.35277777,105.01126,165.48796)">
|
||||
<g
|
||||
id="g16"
|
||||
clip-path="url(#clipPath20)">
|
||||
@ -120,18 +120,31 @@
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:62.089px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:1.5875px;fill:#2f855a;fill-opacity:1;stroke:none;stroke-width:0.264583"
|
||||
x="127.21814"
|
||||
y="-26.413651"
|
||||
id="text856"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan854"
|
||||
x="127.21814"
|
||||
y="-26.413651"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:62.089px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#2f855a;fill-opacity:1;stroke-width:0.264583"
|
||||
dx="0"
|
||||
rotate="0 0 0 0 0 0">akapi</tspan></text>
|
||||
<g
|
||||
aria-label="akapi"
|
||||
transform="matrix(0.35277777,0,0,0.35277777,121.30914,170.32937)"
|
||||
id="text856"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:7.5px;fill:#2f855a;fill-opacity:1;stroke:none;stroke-width:0.75">
|
||||
<path
|
||||
d="m 565.57956,-313.41107 q -1.375,-2.66406 -2.40625,-8.67969 -9.96875,10.39844 -24.40625,10.39844 -14.00781,0 -22.85938,-7.99219 -8.85156,-7.99219 -8.85156,-19.76562 0,-14.86719 11,-22.77344 11.08594,-7.99219 31.625,-7.99219 h 12.80469 v -6.10156 q 0,-7.21875 -4.03906,-11.51563 -4.03907,-4.38281 -12.28907,-4.38281 -7.13281,0 -11.6875,3.60938 -4.55468,3.52343 -4.55468,9.02343 h -20.88282 q 0,-7.64843 5.07032,-14.26562 5.07031,-6.70313 13.75,-10.48438 8.76562,-3.78125 19.50781,-3.78125 16.32812,0 26.03906,8.25 9.71094,8.16407 9.96875,23.03125 v 41.9375 q 0,12.54688 3.52344,20.02344 v 1.46094 z m -22.94531,-15.03906 q 6.1875,0 11.60156,-3.00782 5.5,-3.00781 8.25,-8.07812 v -17.53125 H 551.228 q -11.60157,0 -17.44532,4.03906 -5.84375,4.03906 -5.84375,11.42969 0,6.01562 3.95313,9.625 4.03906,3.52344 10.74219,3.52344 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#2f855a;fill-opacity:1;stroke-width:0.75"
|
||||
id="path851" />
|
||||
<path
|
||||
d="m 642.94675,-353.28607 -9.28125,9.53906 v 30.33594 h -20.88282 v -132 h 20.88282 v 76.14062 l 6.53125,-8.16406 25.69531,-28.96094 h 25.09375 l -34.54688,38.75782 38.24219,54.22656 h -24.14844 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#2f855a;fill-opacity:1;stroke-width:0.75"
|
||||
id="path853" />
|
||||
<path
|
||||
d="m 767.6655,-313.41107 q -1.375,-2.66406 -2.40625,-8.67969 -9.96875,10.39844 -24.40625,10.39844 -14.00782,0 -22.85938,-7.99219 -8.85156,-7.99219 -8.85156,-19.76562 0,-14.86719 11,-22.77344 11.08594,-7.99219 31.625,-7.99219 h 12.80469 v -6.10156 q 0,-7.21875 -4.03907,-11.51563 -4.03906,-4.38281 -12.28906,-4.38281 -7.13281,0 -11.6875,3.60938 -4.55469,3.52343 -4.55469,9.02343 h -20.88281 q 0,-7.64843 5.07031,-14.26562 5.07032,-6.70313 13.75,-10.48438 8.76563,-3.78125 19.50782,-3.78125 16.32812,0 26.03906,8.25 9.71094,8.16407 9.96875,23.03125 v 41.9375 q 0,12.54688 3.52344,20.02344 v 1.46094 z m -22.94532,-15.03906 q 6.1875,0 11.60157,-3.00782 5.5,-3.00781 8.25,-8.07812 v -17.53125 h -11.25782 q -11.60156,0 -17.44531,4.03906 -5.84375,4.03906 -5.84375,11.42969 0,6.01562 3.95313,9.625 4.03906,3.52344 10.74218,3.52344 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#2f855a;fill-opacity:1;stroke-width:0.75"
|
||||
id="path855" />
|
||||
<path
|
||||
d="m 896.25143,-358.95795 q 0,21.57032 -9.79687,34.46094 -9.79688,12.80469 -26.29688,12.80469 -15.29687,0 -24.49218,-10.05469 v 44.08594 h -20.88282 v -128.73438 h 19.25 l 0.85938,9.45313 q 9.19531,-11.17188 25.00781,-11.17188 17.01563,0 26.64063,12.71875 9.71093,12.63282 9.71093,35.14844 z m -20.79687,-1.80468 q 0,-13.92188 -5.58594,-22.08594 -5.5,-8.16406 -15.8125,-8.16406 -12.80469,0 -18.39062,10.57031 v 41.25 q 5.67187,10.82812 18.5625,10.82812 9.96875,0 15.55468,-7.99218 5.67188,-8.07813 5.67188,-24.40625 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#2f855a;fill-opacity:1;stroke-width:0.75"
|
||||
id="path857" />
|
||||
<path
|
||||
d="m 943.62643,-313.41107 h -20.88281 v -92.98438 h 20.88281 z m -22.17187,-117.13281 q 0,-4.8125 3.00781,-7.99219 3.09375,-3.17969 8.76563,-3.17969 5.67187,0 8.76562,3.17969 3.09375,3.17969 3.09375,7.99219 0,4.72656 -3.09375,7.90625 -3.09375,3.09375 -8.76562,3.09375 -5.67188,0 -8.76563,-3.09375 -3.00781,-3.17969 -3.00781,-7.90625 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#2f855a;fill-opacity:1;stroke-width:0.75"
|
||||
id="path859" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 10 KiB |
@ -7,15 +7,15 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="271.70874mm"
|
||||
width="275.9418mm"
|
||||
height="107.06042mm"
|
||||
viewBox="0 0 271.70874 107.06042"
|
||||
viewBox="0 0 275.9418 107.06042"
|
||||
version="1.1"
|
||||
id="svg2814"
|
||||
id="svg900"
|
||||
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
||||
sodipodi:docname="logo-dark-bg.svg">
|
||||
<defs
|
||||
id="defs2808">
|
||||
id="defs894">
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath20">
|
||||
@ -32,19 +32,19 @@
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="-43.677516"
|
||||
inkscape:cy="622.31888"
|
||||
inkscape:cx="501.46484"
|
||||
inkscape:cy="865.17604"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1346"
|
||||
inkscape:window-height="1198"
|
||||
inkscape:window-x="99"
|
||||
inkscape:window-y="63"
|
||||
inkscape:window-x="149"
|
||||
inkscape:window-y="113"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata2811">
|
||||
id="metadata897">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
@ -59,10 +59,10 @@
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-117.38968,16.488537)">
|
||||
transform="translate(26.845906,80.744493)">
|
||||
<g
|
||||
id="g14"
|
||||
transform="matrix(0.35277777,0,0,-0.35277777,43.627792,160.95225)">
|
||||
transform="matrix(0.35277777,0,0,-0.35277777,-100.60779,96.696294)">
|
||||
<g
|
||||
id="g16"
|
||||
clip-path="url(#clipPath20)">
|
||||
@ -120,18 +120,31 @@
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:62.089px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:1.5875px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
|
||||
x="236.07529"
|
||||
y="55.229206"
|
||||
id="text856"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan854"
|
||||
x="236.07529"
|
||||
y="55.229206"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:62.089px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
|
||||
dx="0"
|
||||
rotate="0 0 0 0 0 0">akapi</tspan></text>
|
||||
<g
|
||||
aria-label="akapi"
|
||||
transform="matrix(0.35277777,0,0,0.35277777,-84.30991,101.5377)"
|
||||
id="text856"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:7.5px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.75">
|
||||
<path
|
||||
d="m 565.57956,-313.41107 q -1.375,-2.66406 -2.40625,-8.67969 -9.96875,10.39844 -24.40625,10.39844 -14.00781,0 -22.85938,-7.99219 -8.85156,-7.99219 -8.85156,-19.76562 0,-14.86719 11,-22.77344 11.08594,-7.99219 31.625,-7.99219 h 12.80469 v -6.10156 q 0,-7.21875 -4.03906,-11.51563 -4.03907,-4.38281 -12.28907,-4.38281 -7.13281,0 -11.6875,3.60938 -4.55468,3.52343 -4.55468,9.02343 h -20.88282 q 0,-7.64843 5.07032,-14.26562 5.07031,-6.70313 13.75,-10.48438 8.76562,-3.78125 19.50781,-3.78125 16.32812,0 26.03906,8.25 9.71094,8.16407 9.96875,23.03125 v 41.9375 q 0,12.54688 3.52344,20.02344 v 1.46094 z m -22.94531,-15.03906 q 6.1875,0 11.60156,-3.00782 5.5,-3.00781 8.25,-8.07812 v -17.53125 H 551.228 q -11.60157,0 -17.44532,4.03906 -5.84375,4.03906 -5.84375,11.42969 0,6.01562 3.95313,9.625 4.03906,3.52344 10.74219,3.52344 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke-width:0.75"
|
||||
id="path851" />
|
||||
<path
|
||||
d="m 642.94675,-353.28607 -9.28125,9.53906 v 30.33594 h -20.88282 v -132 h 20.88282 v 76.14062 l 6.53125,-8.16406 25.69531,-28.96094 h 25.09375 l -34.54688,38.75782 38.24219,54.22656 h -24.14844 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke-width:0.75"
|
||||
id="path853" />
|
||||
<path
|
||||
d="m 767.6655,-313.41107 q -1.375,-2.66406 -2.40625,-8.67969 -9.96875,10.39844 -24.40625,10.39844 -14.00782,0 -22.85938,-7.99219 -8.85156,-7.99219 -8.85156,-19.76562 0,-14.86719 11,-22.77344 11.08594,-7.99219 31.625,-7.99219 h 12.80469 v -6.10156 q 0,-7.21875 -4.03907,-11.51563 -4.03906,-4.38281 -12.28906,-4.38281 -7.13281,0 -11.6875,3.60938 -4.55469,3.52343 -4.55469,9.02343 h -20.88281 q 0,-7.64843 5.07031,-14.26562 5.07032,-6.70313 13.75,-10.48438 8.76563,-3.78125 19.50782,-3.78125 16.32812,0 26.03906,8.25 9.71094,8.16407 9.96875,23.03125 v 41.9375 q 0,12.54688 3.52344,20.02344 v 1.46094 z m -22.94532,-15.03906 q 6.1875,0 11.60157,-3.00782 5.5,-3.00781 8.25,-8.07812 v -17.53125 h -11.25782 q -11.60156,0 -17.44531,4.03906 -5.84375,4.03906 -5.84375,11.42969 0,6.01562 3.95313,9.625 4.03906,3.52344 10.74218,3.52344 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke-width:0.75"
|
||||
id="path855" />
|
||||
<path
|
||||
d="m 896.25143,-358.95795 q 0,21.57032 -9.79687,34.46094 -9.79688,12.80469 -26.29688,12.80469 -15.29687,0 -24.49218,-10.05469 v 44.08594 h -20.88282 v -128.73438 h 19.25 l 0.85938,9.45313 q 9.19531,-11.17188 25.00781,-11.17188 17.01563,0 26.64063,12.71875 9.71093,12.63282 9.71093,35.14844 z m -20.79687,-1.80468 q 0,-13.92188 -5.58594,-22.08594 -5.5,-8.16406 -15.8125,-8.16406 -12.80469,0 -18.39062,10.57031 v 41.25 q 5.67187,10.82812 18.5625,10.82812 9.96875,0 15.55468,-7.99218 5.67188,-8.07813 5.67188,-24.40625 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke-width:0.75"
|
||||
id="path857" />
|
||||
<path
|
||||
d="m 943.62643,-313.41107 h -20.88281 v -92.98438 h 20.88281 z m -22.17187,-117.13281 q 0,-4.8125 3.00781,-7.99219 3.09375,-3.17969 8.76563,-3.17969 5.67187,0 8.76562,3.17969 3.09375,3.17969 3.09375,7.99219 0,4.72656 -3.09375,7.90625 -3.09375,3.09375 -8.76562,3.09375 -5.67188,0 -8.76563,-3.09375 -3.00781,-3.17969 -3.00781,-7.90625 z"
|
||||
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:176px;font-family:Roboto;-inkscape-font-specification:'Roboto, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;fill-opacity:1;stroke-width:0.75"
|
||||
id="path859" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 10 KiB |
1
static/assets/images/welcome.svg
Normal file
1
static/assets/images/welcome.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 12 KiB |
@ -36,7 +36,7 @@ func ExtractBasicAuth(r *http.Request) (username, password string, err error) {
|
||||
|
||||
func ExtractBearerAuth(r *http.Request) (key string, err error) {
|
||||
authHeader := strings.Split(r.Header.Get("Authorization"), " ")
|
||||
if len(authHeader) != 2 || authHeader[0] != "Basic" {
|
||||
if len(authHeader) != 2 || (authHeader[0] != "Basic" && authHeader[0] != "Bearer") {
|
||||
return key, errors.New("failed to extract API key")
|
||||
}
|
||||
|
||||
|
@ -2,16 +2,25 @@ package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/muety/wakapi/config"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ParseDate(date string) (time.Time, error) {
|
||||
return time.Parse("2006-01-02 15:04:05", date)
|
||||
return time.Parse(config.SimpleDateFormat, date)
|
||||
}
|
||||
|
||||
func ParseDateTime(date string) (time.Time, error) {
|
||||
return time.Parse(config.SimpleDateTimeFormat, date)
|
||||
}
|
||||
|
||||
func FormatDate(date time.Time) string {
|
||||
return date.Format("2006-01-02 15:04:05")
|
||||
return date.Format(config.SimpleDateFormat)
|
||||
}
|
||||
|
||||
func FormatDateTime(date time.Time) string {
|
||||
return date.Format(config.SimpleDateTimeFormat)
|
||||
}
|
||||
|
||||
func FormatDateHuman(date time.Time) string {
|
||||
|
@ -16,6 +16,11 @@ func ParseInterval(interval string) (*models.IntervalKey, error) {
|
||||
return nil, errors.New("not a valid interval")
|
||||
}
|
||||
|
||||
func MustResolveIntervalRaw(interval string) (from, to time.Time) {
|
||||
_, from, to = ResolveIntervalRaw(interval)
|
||||
return from, to
|
||||
}
|
||||
|
||||
func ResolveIntervalRaw(interval string) (err error, from, to time.Time) {
|
||||
parsed, err := ParseInterval(interval)
|
||||
if err != nil {
|
||||
@ -74,15 +79,23 @@ func ParseSummaryParams(r *http.Request) (*models.SummaryParams, error) {
|
||||
|
||||
if interval := params.Get("interval"); interval != "" {
|
||||
err, from, to = ResolveIntervalRaw(interval)
|
||||
} else if start := params.Get("start"); start != "" {
|
||||
err, from, to = ResolveIntervalRaw(start)
|
||||
} else {
|
||||
from, err = ParseDate(params.Get("from"))
|
||||
from, err = ParseDateTime(params.Get("from"))
|
||||
if err != nil {
|
||||
return nil, errors.New("missing 'from' parameter")
|
||||
from, err = ParseDate(params.Get("from"))
|
||||
if err != nil {
|
||||
return nil, errors.New("missing 'from' parameter")
|
||||
}
|
||||
}
|
||||
|
||||
to, err = ParseDate(params.Get("to"))
|
||||
to, err = ParseDateTime(params.Get("to"))
|
||||
if err != nil {
|
||||
return nil, errors.New("missing 'to' parameter")
|
||||
to, err = ParseDate(params.Get("to"))
|
||||
if err != nil {
|
||||
return nil, errors.New("missing 'to' parameter")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1 +1 @@
|
||||
1.23.3
|
||||
1.24.3
|
||||
|
@ -9,4 +9,26 @@
|
||||
<div>
|
||||
<a href="imprint" class="border-b border-green-700">Imprint, Cookies & Data Privacy</a>
|
||||
</div>
|
||||
</footer>
|
||||
</footer>
|
||||
|
||||
<script type="text/javascript">
|
||||
const baseUrl = location.href.substring(0, location.href.lastIndexOf('/'))
|
||||
document.querySelectorAll('.with-url-src').forEach(e => {
|
||||
e.setAttribute('src', e.getAttribute('src').replace('%s', baseUrl))
|
||||
e.classList.remove('hidden')
|
||||
})
|
||||
document.querySelectorAll('.with-url-src-no-scheme').forEach(e => {
|
||||
const strippedUrl = baseUrl.replace(/https?:\/\//, '')
|
||||
e.setAttribute('src', e.getAttribute('src').replace('%s', strippedUrl))
|
||||
e.classList.remove('hidden')
|
||||
})
|
||||
document.querySelectorAll('.with-url-inner').forEach(e => {
|
||||
e.innerHTML = e.innerHTML.replace('%s', baseUrl)
|
||||
e.classList.remove('hidden')
|
||||
})
|
||||
document.querySelectorAll('.with-url-inner-no-scheme').forEach(e => {
|
||||
const strippedUrl = baseUrl.replace(/https?:\/\//, '')
|
||||
e.innerHTML = e.innerHTML.replace('%s', strippedUrl)
|
||||
e.classList.remove('hidden')
|
||||
})
|
||||
</script>
|
@ -1,5 +1,5 @@
|
||||
<header class="flex justify-between mb-10">
|
||||
<a id="logo-container" class="text-2xl font-semibold text-white inline-block" href="">
|
||||
<img src="assets/images/logo.svg" width="110px">
|
||||
<img src="assets/images/logo.svg" width="110px" alt="Logo">
|
||||
</a>
|
||||
</header>
|
@ -27,11 +27,11 @@
|
||||
<p class="text-center text-gray-500 text-xl my-4">
|
||||
<span class="mr-1">💡 The system has tracked a total of </span>
|
||||
{{ range $d := .TotalHours | printf "%d" | toRunes }}
|
||||
<span class="bg-gray-900 rounded-sm p-1 border border-gray-700 font-mono" style="margin: auto -2px;" title="{{ $.TotalHours }} hours (updated once a day)">{{ $d }}</span>
|
||||
<span class="bg-gray-900 rounded-sm p-1 border border-gray-700 font-mono" style="margin: auto -2px;" title="{{ $.TotalHours }} hours (updated every hour)">{{ $d }}</span>
|
||||
{{ end }}
|
||||
<span class="mx-1">hours of coding from</span>
|
||||
{{ range $d := .TotalUsers | printf "%d" | toRunes }}
|
||||
<span class="bg-gray-900 rounded-sm p-1 border border-gray-700 font-mono" style="margin: auto -2px;" title="{{ $.TotalUsers }} users (updated once a day)">{{ $d }}</span>
|
||||
<span class="bg-gray-900 rounded-sm p-1 border border-gray-700 font-mono" style="margin: auto -2px;" title="{{ $.TotalUsers }} users (updated every hour)">{{ $d }}</span>
|
||||
{{ end }}
|
||||
<span class="ml-1">users.</span>
|
||||
</p>
|
||||
@ -72,6 +72,7 @@
|
||||
class="underline">Prometheus</a> metrics via <a
|
||||
href="https://github.com/MacroPower/wakatime_exporter" target="_blank"
|
||||
rel="noopener noreferrer" class="underline">exporter</a></li>
|
||||
<li>✅ Lightning fast</li>
|
||||
<li>✅ Self-hosted</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -22,7 +22,7 @@
|
||||
{{ template "header.tpl.html" . }}
|
||||
|
||||
<div class="w-full flex justify-center">
|
||||
<div class="flex items-center justify-between max-w-xl flex-grow">
|
||||
<div class="flex items-center justify-between max-w-2xl flex-grow">
|
||||
<div><a href="" class="text-gray-500 text-sm">← Go back</a></div>
|
||||
<div><h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Settings</h1></div>
|
||||
<div> </div>
|
||||
@ -32,7 +32,7 @@
|
||||
{{ template "alerts.tpl.html" . }}
|
||||
|
||||
<main class="mt-4 flex-grow flex justify-center w-full">
|
||||
<div class="flex flex-col flex-grow max-w-xl mt-8">
|
||||
<div class="flex flex-col flex-grow max-w-2xl mt-8">
|
||||
|
||||
<details class="my-8 pb-8 border-b border-gray-700">
|
||||
<summary class="cursor-pointer">
|
||||
@ -534,26 +534,6 @@
|
||||
</main>
|
||||
|
||||
<script type="text/javascript">
|
||||
const baseUrl = location.href.substring(0, location.href.indexOf('/settings'))
|
||||
document.querySelectorAll('.with-url-src').forEach(e => {
|
||||
e.setAttribute('src', e.getAttribute('src').replace('%s', baseUrl))
|
||||
e.classList.remove('hidden')
|
||||
})
|
||||
document.querySelectorAll('.with-url-src-no-scheme').forEach(e => {
|
||||
const strippedUrl = baseUrl.replace(/https?:\/\//, '')
|
||||
e.setAttribute('src', e.getAttribute('src').replace('%s', strippedUrl))
|
||||
e.classList.remove('hidden')
|
||||
})
|
||||
document.querySelectorAll('.with-url-inner').forEach(e => {
|
||||
e.innerHTML = e.innerHTML.replace('%s', baseUrl)
|
||||
e.classList.remove('hidden')
|
||||
})
|
||||
document.querySelectorAll('.with-url-inner-no-scheme').forEach(e => {
|
||||
const strippedUrl = baseUrl.replace(/https?:\/\//, '')
|
||||
e.innerHTML = e.innerHTML.replace('%s', strippedUrl)
|
||||
e.classList.remove('hidden')
|
||||
})
|
||||
|
||||
const btnRegenerate = document.querySelector('#btn-regenerate-summaries')
|
||||
const formRegenerate = document.querySelector('#form-regenerate-summaries')
|
||||
btnRegenerate.addEventListener('click', () => {
|
||||
|
@ -49,6 +49,13 @@
|
||||
type="password" id="password_repeat"
|
||||
name="password_repeat" placeholder="Repeat your password" minlength="6" required>
|
||||
</div>
|
||||
|
||||
{{ if eq .TotalUsers 0 }}
|
||||
<p class="text-sm text-gray-300 mt-4 mb-8">
|
||||
⚠️ <strong>Please note: </strong> Since there are no users registered in the system, yet, the first user will have administrative privileges, while additional users won't.
|
||||
</p>
|
||||
{{ end }}
|
||||
|
||||
<div class="flex justify-between float-right">
|
||||
<button type="submit" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
||||
Create Account
|
||||
|
@ -36,33 +36,57 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center">
|
||||
<h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Your Coding Statistics 🤓</h1>
|
||||
<h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Summary</h1>
|
||||
</div>
|
||||
|
||||
<div class="text-white text-sm flex items-center justify-center mt-4 self-center max-w-lg flex-wrap">
|
||||
<a href="summary?interval=today" class="mx-2 my-1 border-b border-green-700">Today</a>
|
||||
<a href="summary?interval=yesterday" class="mx-2 my-1 border-b border-green-700">Yesterday</a>
|
||||
<a href="summary?interval=week" class="mx-2 my-1 border-b border-green-700">This Week</a>
|
||||
<a href="summary?interval=month" class="mx-2 my-1 border-b border-green-700">This Month</a>
|
||||
<a href="summary?interval=year" class="mx-2 my-1 border-b border-green-700">This Year</a>
|
||||
<a href="summary?interval=last_7_days" class="mx-2 my-1 border-b border-green-700">Past 7 Days</a>
|
||||
<a href="summary?interval=last_30_days" class="mx-2 my-1 border-b border-green-700">Past 30 Days</a>
|
||||
<a href="summary?interval=last_12_months" class="mx-2 my-1 border-b border-green-700">Past 12 Months</a>
|
||||
<a href="summary?interval=any" class="mx-2 my-1 border-b border-green-700">All Time</a>
|
||||
{{ if .User.HasData }}
|
||||
|
||||
<div class="self-center border border-gray-700 shadow mt-8 rounded-md p-4 bg-gray-900">
|
||||
<form class="text-white flex flex-nowrap items-center justify-center self-center max-w-xl flex-wrap space-x-8">
|
||||
<div class="flex space-x-1">
|
||||
<label for="from-date-picker" class="text-gray-300 pl-1">▶️ Start:</label>
|
||||
<input id="from-date-picker" type="date" name="from" class="text-sm text-gray-300 bg-gray-800 rounded-md text-center cursor-pointer"
|
||||
value="{{ .FromTime.T | simpledate }}" required>
|
||||
</div>
|
||||
<div class="flex space-x-1">
|
||||
<label for="to-date-picker" class="text-gray-300 pl-1">⏹️ End:</label>
|
||||
<input id="to-date-picker" type="date" name="to" class="text-sm text-gray-300 bg-gray-800 rounded-md text-center cursor-pointer"
|
||||
value="{{ .ToTime.T | simpledate }}" required>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">Show</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="text-gray-300 text-sm flex items-center justify-center mt-4 self-center max-w-lg flex-wrap">
|
||||
<a href="summary?interval=today" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">Today</a>
|
||||
<a href="summary?interval=yesterday" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">Yesterday</a>
|
||||
<a href="summary?interval=week" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">This Week</a>
|
||||
<a href="summary?interval=month" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">This Month</a>
|
||||
<a href="summary?interval=year" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">This Year</a>
|
||||
<a href="summary?interval=last_7_days" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">Past 7 Days</a>
|
||||
<a href="summary?interval=last_30_days" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">Past 30 Days</a>
|
||||
<a href="summary?interval=last_12_months" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">Past 12 Months</a>
|
||||
<a href="summary?interval=any" class="px-1 my-1 mx-1 border-b hover:border-b-2 border-gray-700 hover:bg-green-700 rounded hover:border-none">All Time</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ end }}
|
||||
|
||||
{{ template "alerts.tpl.html" . }}
|
||||
|
||||
<main class="mt-10 flex-grow">
|
||||
<div class="flex justify-center">
|
||||
<div class="p-1">
|
||||
<div class="flex justify-center p-4 bg-gray-900 border border-gray-700 text-gray-300 rounded-md shadow">
|
||||
<p class="mx-2"><strong>▶️</strong> <span title="Start Time">{{ .FromTime.T | date }}</span></p>
|
||||
<p class="mx-2"><strong>⏹️</strong> <span title="End Time">{{ .ToTime.T | date }}</span></p>
|
||||
<p class="mx-2"><strong>⏱️</strong> <span id="total-span" title="Total Hours"></span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<main class="flex flex-col items-center mt-10 flex-grow">
|
||||
|
||||
{{ if .User.HasData }}
|
||||
|
||||
<span class="text-white text-lg text-gray-300 text-center mb-4">
|
||||
<span class="text-xl">⏱️ </span>
|
||||
Showing a total of <span id="total-span" title="Total Hours" class="text-white text-xl font-semibold border-b-2 border-green-700"></span>
|
||||
<span class="text-sm my-2">
|
||||
(from <span title="Start Time" class="border-b border-gray-700">{{ .FromTime.T | date }}</span> to <span title="End Time" class="border-b border-gray-700">{{ .ToTime.T | date }}</span>)
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="flex flex-wrap justify-center">
|
||||
<div class="w-full lg:w-1/2 p-1">
|
||||
<div class="p-4 pb-10 bg-gray-900 border border-gray-700 text-gray-300 rounded-md shadow m-2 flex flex-col" id="project-container" style="height: 300px">
|
||||
@ -76,8 +100,7 @@
|
||||
</div>
|
||||
<canvas id="chart-projects" class="mt-2"></canvas>
|
||||
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
|
||||
<img src="assets/images/no_data.svg" class="w-20" alt="No data"/>
|
||||
<span class="text-sm mt-4">No data available ...</span>
|
||||
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -93,8 +116,7 @@
|
||||
</div>
|
||||
<canvas id="chart-os"></canvas>
|
||||
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
|
||||
<img src="assets/images/no_data.svg" class="w-20" alt="No data"/>
|
||||
<span class="text-sm mt-4">No data available ...</span>
|
||||
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -110,8 +132,7 @@
|
||||
</div>
|
||||
<canvas id="chart-language"></canvas>
|
||||
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
|
||||
<img src="assets/images/no_data.svg" class="w-20" alt="No data"/>
|
||||
<span class="text-sm mt-4">No data available ...</span>
|
||||
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -127,8 +148,7 @@
|
||||
</div>
|
||||
<canvas id="chart-editor"></canvas>
|
||||
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
|
||||
<img src="assets/images/no_data.svg" class="w-20" alt="No data"/>
|
||||
<span class="text-sm mt-4">No data available ...</span>
|
||||
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -144,18 +164,55 @@
|
||||
</div>
|
||||
<canvas id="chart-machine"></canvas>
|
||||
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
|
||||
<img src="assets/images/no_data.svg" class="w-20" alt="No data"/>
|
||||
<span class="text-sm mt-4">No data available ...</span>
|
||||
<span class="text-md font-semibold text-gray-500 mt-4">No data</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ else }}
|
||||
|
||||
<div class="max-w-screen-sm flex flex-col items-center mt-12 space-y-8 text-gray-300 text-center">
|
||||
<div class="pb-4">
|
||||
<img src="assets/images/welcome.svg" width="200px" alt="User welcome illustration">
|
||||
</div>
|
||||
<p class="text-sm">
|
||||
<strong>Welcome to Wakapi! 👋</strong> It looks like there is no data available for the specified time range.<br>If you logged in to Wakapi for the first time, see the setup instructions below on how to get started.
|
||||
</p>
|
||||
<div class="w-full pt-10 flex flex-col space-y-4">
|
||||
<div>
|
||||
<h3 class="inline-block font-semibold text-md border-b border-green-700">Setup Instructions</h3>
|
||||
</div>
|
||||
<div class="w-full bg-gray-900 text-left rounded-md py-4 px-8 text-xs font-mono shadow-md">
|
||||
# <strong>Step 1:</strong> Download WakaTime plugin for your IDE<br>
|
||||
# See: https://wakatime.com/plugins<br><br>
|
||||
|
||||
# <strong>Step 2:</strong> Adapt your config<br>
|
||||
$ vi ~/.wakatime.cfg<br>
|
||||
|
||||
# Set <em>api_url = <span class="with-url-inner">%s/api/heartbeat</span></em><br>
|
||||
# Set <em>api_key = <span id="api-key-instruction"></span></em><br><br>
|
||||
|
||||
# <strong>Step 3:</strong> Start coding and then check back here!
|
||||
</div>
|
||||
<p class="pt-4 text-sm">
|
||||
More at <a href="https://github.com/muety/wakapi" target="_blank" rel="noreferrer noopener" class="font-mono border-b border-green-700">github.com/muety/wakapi</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ end }}
|
||||
|
||||
</main>
|
||||
|
||||
{{ template "footer.tpl.html" . }}
|
||||
|
||||
{{ template "foot.tpl.html" . }}
|
||||
|
||||
<script type="text/javascript">
|
||||
document.querySelector('#api-key-instruction').innerHTML = document.querySelector('#api-key-container').value
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const languageColors = {{ .LanguageColors | json }}
|
||||
const editorColors = {{ .EditorColors | json }}
|
||||
|
Reference in New Issue
Block a user