mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
2b57da224c | |||
01d51b78b1 | |||
6b83600acc | |||
65bbd744b5 | |||
81ca703501 | |||
2d1010e9d9 | |||
5ca9a6a8be | |||
caf87de887 | |||
9fc3c65efe | |||
f73285160d | |||
1f557d562f | |||
3685f3a156 | |||
b3afe9bfa2 | |||
9de2c20885 | |||
2846748b26 | |||
f2f6fe1483 | |||
17ddd7ca76 | |||
292ae41c58 | |||
4f86f67716 | |||
017530ac4a | |||
81d3251856 | |||
16af17fc37 | |||
701ed0a3e1 | |||
218c93e975 | |||
44de057022 | |||
e55adf6287 | |||
56800be8e8 | |||
c149766ecc | |||
759e8e4dfd | |||
708863fd33 | |||
e2f046a83d | |||
30510591eb | |||
daf67b844a | |||
6b0b3bddda | |||
ef17d06763 | |||
301cab4be4 | |||
703805412b | |||
88eb68b1a9 | |||
8191a52ce1 | |||
5b3e88247e | |||
59b85863cc | |||
22fbfceca2 | |||
4d7fc6bff9 | |||
218c571859 | |||
e4c413a33c | |||
66b01c2797 | |||
0cee7496e0 |
54
README.md
54
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
|
||||
@ -90,6 +90,8 @@ $ docker run -d \
|
||||
|
||||
**Note:** By default, SQLite is used as a database. To run Wakapi in Docker with MySQL or Postgres, see [Dockerfile](https://github.com/muety/wakapi/blob/master/Dockerfile) and [config.default.yml](https://github.com/muety/wakapi/blob/master/config.default.yml) for further options.
|
||||
|
||||
If you want to run Wakapi on **Kubernetes**, there is [wakapi-helm-chart](https://github.com/andreymaznyak/wakapi-helm-chart) for quick and easy deployment.
|
||||
|
||||
### 📦 Option 3: Run a release
|
||||
```bash
|
||||
# Download the release and unpack it
|
||||
@ -164,15 +166,22 @@ 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 |
|
||||
| `db.password` | `WAKAPI_DB_PASSWORD` | - | Database password |
|
||||
| `db.name` | `WAKAPI_DB_NAME` | `wakapi_db.db` | Database name |
|
||||
| `db.dialect` | `WAKAPI_DB_TYPE` | `sqlite3` | Database type (one of `sqlite3`, `mysql`, `postgres`, `cockroach`) |
|
||||
| `db.dialect` | `WAKAPI_DB_TYPE` | `sqlite3` | Database type (one of `sqlite3`, `mysql`, `postgres`, `cockroach`) |
|
||||
| `db.charset` | `WAKAPI_DB_CHARSET` | `utf8mb4` | Database connection charset (for MySQL only) |
|
||||
| `db.max_conn` | `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections |
|
||||
| `db.ssl` | `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) |
|
||||
| `sentry.dsn` | `WAKAPI_SENTRY_DSN` | – | DSN for to integrate [Sentry](https://sentry.io) for error logging and tracing (leave empty to disable) |
|
||||
| `sentry.enable_tracing` | `WAKAPI_SENTRY_TRACING` | `false` | Whether to enable Sentry request tracing |
|
||||
| `sentry.sample_rate` | `WAKAPI_SENTRY_SAMPLE_RATE` | `0.75` | Probability of tracing a request in Sentry |
|
||||
| `sentry.sample_rate_heartbats` | `WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS` | `0.1` | Probability of tracing a heartbeats request in Sentry |
|
||||
|
||||
### Supported databases
|
||||
Wakapi uses [GORM](https://gorm.io) as an ORM. As a consequence, a set of different relational databases is supported.
|
||||
@ -196,13 +205,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.
|
||||
@ -222,6 +255,13 @@ However, if you want to expose your wakapi instance to the public anyway, you ne
|
||||
CGO_FLAGS="-g -O2 -Wno-return-local-addr" go test -json -coverprofile=coverage/coverage.out ./... -run ./...
|
||||
```
|
||||
|
||||
### Building Tailwind
|
||||
To keep things minimal, Wakapi does not contain a `package.json`, `node_modules` or any sort of frontend build step. Instead, all JS and CSS assets are included as static files and checked in to Git. This way we can avoid requiring NodeJS to build Wakapi. However, for [TailwindCSS](https://tailwindcss.com/docs/installation#building-for-production) it makes sense to run it through a "build" step to benefit from purging and significantly reduce it in size. To only require this at the time of development, the compiled asset is checked in to Git as well.
|
||||
|
||||
```bash
|
||||
$ tailwindcss-cli build static/assets/vendor/tailwind.css -o static/assets/vendor/tailwind.dist.css
|
||||
```
|
||||
|
||||
## 🙏 Support
|
||||
If you like this project, please consider supporting it 🙂. You can donate either through [buying me a coffee](https://buymeacoff.ee/n1try) or becoming a GitHub sponsor. Every little donation is highly appreciated and boosts the developers' motivation to keep improving Wakapi!
|
||||
|
||||
|
@ -10,10 +10,11 @@ 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
|
||||
svelte: Svelte
|
||||
|
||||
db:
|
||||
host: # leave blank when using sqlite3
|
||||
@ -22,11 +23,19 @@ db:
|
||||
password: # leave blank when using sqlite3
|
||||
name: wakapi_db.db # database name for mysql / postgres or file path for sqlite (e.g. /tmp/wakapi.db)
|
||||
dialect: sqlite3 # mysql, postgres, sqlite3
|
||||
charset: utf8mb4 # only used for mysql connections
|
||||
max_conn: 2 # maximum number of concurrent connections to maintain
|
||||
ssl: false # whether to use tls for db connection (must be true for cockroachdb) (ignored for mysql and sqlite)
|
||||
|
||||
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
|
||||
|
||||
sentry:
|
||||
dsn: # leave blank to disable sentry integration
|
||||
enable_tracing: true # whether to use performance monitoring
|
||||
sample_rate: 0.75 # probability of tracing a request
|
||||
sample_rate_heartbeats: 0.1 # probability of tracing a heartbeat request
|
@ -5,6 +5,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/jinzhu/configor"
|
||||
"github.com/markbates/pkger"
|
||||
@ -30,6 +31,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 +46,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 +54,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"`
|
||||
@ -69,6 +78,7 @@ type dbConfig struct {
|
||||
Password string `env:"WAKAPI_DB_PASSWORD"`
|
||||
Name string `default:"wakapi_db.db" env:"WAKAPI_DB_NAME"`
|
||||
Dialect string `yaml:"-"`
|
||||
Charset string `default:"utf8mb4" env:"WAKAPI_DB_CHARSET"`
|
||||
Type string `yaml:"dialect" default:"sqlite3" env:"WAKAPI_DB_TYPE"`
|
||||
MaxConn uint `yaml:"max_conn" default:"2" env:"WAKAPI_DB_MAX_CONNECTIONS"`
|
||||
Ssl bool `default:"false" env:"WAKAPI_DB_SSL"`
|
||||
@ -83,6 +93,13 @@ type serverConfig struct {
|
||||
TlsKeyPath string `yaml:"tls_key_path" default:"" env:"WAKAPI_TLS_KEY_PATH"`
|
||||
}
|
||||
|
||||
type sentryConfig struct {
|
||||
Dsn string `env:"WAKAPI_SENTRY_DSN"`
|
||||
EnableTracing bool `yaml:"enable_tracing" env:"WAKAPI_SENTRY_TRACING"`
|
||||
SampleRate float32 `yaml:"sample_rate" default:"0.75" env:"WAKAPI_SENTRY_SAMPLE_RATE"`
|
||||
SampleRateHeartbeats float32 `yaml:"sample_rate_heartbeats" default:"0.1" env:"WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Env string `default:"dev" env:"ENVIRONMENT"`
|
||||
Version string `yaml:"-"`
|
||||
@ -90,6 +107,7 @@ type Config struct {
|
||||
Security securityConfig
|
||||
Db dbConfig
|
||||
Server serverConfig
|
||||
Sentry sentryConfig
|
||||
}
|
||||
|
||||
func (c *Config) CreateCookie(name, value, path string) *http.Cookie {
|
||||
@ -173,12 +191,13 @@ func (c *dbConfig) GetDialector() gorm.Dialector {
|
||||
|
||||
func mysqlConnectionString(config *dbConfig) string {
|
||||
//location, _ := time.LoadLocation("Local")
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
|
||||
config.User,
|
||||
config.Password,
|
||||
config.Host,
|
||||
config.Port,
|
||||
config.Name,
|
||||
config.Charset,
|
||||
"Local",
|
||||
)
|
||||
}
|
||||
@ -280,6 +299,44 @@ func resolveDbDialect(dbType string) string {
|
||||
return dbType
|
||||
}
|
||||
|
||||
func initSentry(config sentryConfig, debug bool) {
|
||||
if err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: config.Dsn,
|
||||
Debug: debug,
|
||||
TracesSampler: sentry.TracesSamplerFunc(func(ctx sentry.SamplingContext) sentry.Sampled {
|
||||
if !config.EnableTracing {
|
||||
return sentry.SampledFalse
|
||||
}
|
||||
|
||||
hub := sentry.GetHubFromContext(ctx.Span.Context())
|
||||
txName := hub.Scope().Transaction()
|
||||
|
||||
if strings.HasPrefix(txName, "GET /assets") {
|
||||
return sentry.SampledFalse
|
||||
}
|
||||
if txName == "POST /api/heartbeat" {
|
||||
return sentry.UniformTracesSampler(config.SampleRateHeartbeats).Sample(ctx)
|
||||
}
|
||||
return sentry.UniformTracesSampler(config.SampleRate).Sample(ctx)
|
||||
}),
|
||||
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
|
||||
type principalGetter interface {
|
||||
GetPrincipal() *models.User
|
||||
}
|
||||
if hint.Context != nil {
|
||||
if req, ok := hint.Context.Value(sentry.RequestContextKey).(*http.Request); ok {
|
||||
if p := req.Context().Value("principal"); p != nil {
|
||||
event.User.ID = p.(principalGetter).GetPrincipal().ID
|
||||
}
|
||||
}
|
||||
}
|
||||
return event
|
||||
},
|
||||
}); err != nil {
|
||||
logbuch.Fatal("failed to initialized sentry – %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Set(config *Config) {
|
||||
cfg = config
|
||||
}
|
||||
@ -323,6 +380,11 @@ func Load() *Config {
|
||||
logbuch.Fatal("you must allow at least one database connection")
|
||||
}
|
||||
|
||||
if config.Sentry.Dsn != "" {
|
||||
logbuch.Info("enabling sentry integration")
|
||||
initSentry(config.Sentry, config.IsDev())
|
||||
}
|
||||
|
||||
Set(config)
|
||||
return Get()
|
||||
}
|
||||
|
@ -22,11 +22,12 @@ func Test_mysqlConnectionString(t *testing.T) {
|
||||
Password: "test_password",
|
||||
Name: "test_name",
|
||||
Dialect: "mysql",
|
||||
Charset: "utf8mb4",
|
||||
MaxConn: 10,
|
||||
}
|
||||
|
||||
assert.Equal(t, fmt.Sprintf(
|
||||
"%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
|
||||
"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
|
||||
c.User,
|
||||
c.Password,
|
||||
c.Host,
|
||||
|
@ -1,30 +1,100 @@
|
||||
mode: set
|
||||
github.com/muety/wakapi/models/heartbeat.go:32.34,34.2 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:36.65,37.46 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:37.46,38.46 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:38.46,41.4 2 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:45.50,46.11 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:59.2,59.15 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:63.2,63.12 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:47.22,48.18 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:49.21,50.17 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:51.23,52.19 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:53.17,54.26 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:55.22,56.18 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:59.15,61.3 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:66.37,82.2 1 0
|
||||
github.com/muety/wakapi/models/heartbeat.go:90.41,92.16 2 0
|
||||
github.com/muety/wakapi/models/heartbeat.go:95.2,96.10 2 0
|
||||
github.com/muety/wakapi/models/heartbeat.go:92.16,94.3 1 0
|
||||
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/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: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/models/summary.go:69.29,71.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:73.37,80.2 6 1
|
||||
github.com/muety/wakapi/models/summary.go:82.35,84.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:86.57,94.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:96.64,98.2 1 0
|
||||
github.com/muety/wakapi/models/summary.go:111.33,116.26 4 1
|
||||
github.com/muety/wakapi/models/summary.go:123.2,123.37 1 1
|
||||
github.com/muety/wakapi/models/summary.go:127.2,130.33 2 1
|
||||
github.com/muety/wakapi/models/summary.go:116.26,117.30 1 1
|
||||
github.com/muety/wakapi/models/summary.go:117.30,119.4 1 1
|
||||
github.com/muety/wakapi/models/summary.go:123.37,125.3 1 0
|
||||
github.com/muety/wakapi/models/summary.go:130.33,136.3 1 1
|
||||
github.com/muety/wakapi/models/summary.go:139.45,144.30 3 1
|
||||
github.com/muety/wakapi/models/summary.go:153.2,153.30 1 1
|
||||
github.com/muety/wakapi/models/summary.go:144.30,145.47 1 1
|
||||
github.com/muety/wakapi/models/summary.go:145.47,146.32 1 1
|
||||
github.com/muety/wakapi/models/summary.go:149.4,149.9 1 1
|
||||
github.com/muety/wakapi/models/summary.go:146.32,148.5 1 1
|
||||
github.com/muety/wakapi/models/summary.go:156.73,158.55 2 1
|
||||
github.com/muety/wakapi/models/summary.go:163.2,163.16 1 1
|
||||
github.com/muety/wakapi/models/summary.go:158.55,159.31 1 1
|
||||
github.com/muety/wakapi/models/summary.go:159.31,161.4 1 1
|
||||
github.com/muety/wakapi/models/summary.go:166.88,168.55 2 1
|
||||
github.com/muety/wakapi/models/summary.go:176.2,176.16 1 1
|
||||
github.com/muety/wakapi/models/summary.go:168.55,169.31 1 1
|
||||
github.com/muety/wakapi/models/summary.go:169.31,170.23 1 1
|
||||
github.com/muety/wakapi/models/summary.go:173.4,173.46 1 1
|
||||
github.com/muety/wakapi/models/summary.go:170.23,171.13 1 1
|
||||
github.com/muety/wakapi/models/summary.go:179.70,181.8 2 1
|
||||
github.com/muety/wakapi/models/summary.go:184.2,184.10 1 1
|
||||
github.com/muety/wakapi/models/summary.go:181.8,183.3 1 1
|
||||
github.com/muety/wakapi/models/summary.go:187.71,188.63 1 1
|
||||
github.com/muety/wakapi/models/summary.go:228.2,234.10 6 1
|
||||
github.com/muety/wakapi/models/summary.go:188.63,191.45 2 1
|
||||
github.com/muety/wakapi/models/summary.go:200.3,200.31 1 1
|
||||
github.com/muety/wakapi/models/summary.go:207.3,207.31 1 1
|
||||
github.com/muety/wakapi/models/summary.go:224.3,224.16 1 1
|
||||
github.com/muety/wakapi/models/summary.go:191.45,192.32 1 1
|
||||
github.com/muety/wakapi/models/summary.go:197.4,197.14 1 1
|
||||
github.com/muety/wakapi/models/summary.go:192.32,193.24 1 1
|
||||
github.com/muety/wakapi/models/summary.go:193.24,195.6 1 1
|
||||
github.com/muety/wakapi/models/summary.go:200.31,202.60 1 1
|
||||
github.com/muety/wakapi/models/summary.go:202.60,204.5 1 1
|
||||
github.com/muety/wakapi/models/summary.go:207.31,209.60 1 1
|
||||
github.com/muety/wakapi/models/summary.go:209.60,210.55 1 1
|
||||
github.com/muety/wakapi/models/summary.go:210.55,212.6 1 1
|
||||
github.com/muety/wakapi/models/summary.go:212.11,220.6 1 1
|
||||
github.com/muety/wakapi/models/summary.go:237.33,239.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:241.43,243.2 1 1
|
||||
github.com/muety/wakapi/models/summary.go:245.38,247.2 1 1
|
||||
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
|
||||
github.com/muety/wakapi/models/heartbeat.go:44.2,45.44 2 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:48.2,48.42 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:37.28,39.3 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:41.45,43.3 1 0
|
||||
github.com/muety/wakapi/models/heartbeat.go:45.44,47.3 1 0
|
||||
github.com/muety/wakapi/models/heartbeat.go:51.50,52.11 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:65.2,65.15 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:69.2,69.12 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:53.22,54.18 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:55.21,56.17 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:57.23,58.19 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:59.17,60.26 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:61.22,62.18 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:65.15,67.3 1 1
|
||||
github.com/muety/wakapi/models/heartbeat.go:72.37,88.2 1 0
|
||||
github.com/muety/wakapi/models/heartbeat.go:96.41,98.16 2 0
|
||||
github.com/muety/wakapi/models/heartbeat.go:101.2,102.10 2 0
|
||||
github.com/muety/wakapi/models/heartbeat.go:98.16,100.3 1 0
|
||||
github.com/muety/wakapi/models/heartbeats.go:7.31,9.2 1 0
|
||||
github.com/muety/wakapi/models/heartbeats.go:11.41,13.2 1 0
|
||||
github.com/muety/wakapi/models/heartbeats.go:15.36,17.2 1 0
|
||||
@ -35,74 +105,13 @@ github.com/muety/wakapi/models/heartbeats.go:26.18,28.3 1 0
|
||||
github.com/muety/wakapi/models/heartbeats.go:32.40,34.18 1 0
|
||||
github.com/muety/wakapi/models/heartbeats.go:37.2,37.24 1 0
|
||||
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/user.go:13.13,15.2 1 1
|
||||
github.com/muety/wakapi/models/user.go:67.43,70.2 1 0
|
||||
github.com/muety/wakapi/models/user.go:72.33,77.2 1 0
|
||||
github.com/muety/wakapi/models/user.go:79.41,81.2 1 0
|
||||
github.com/muety/wakapi/models/user.go:83.45,85.2 1 0
|
||||
github.com/muety/wakapi/models/user.go:87.45,89.2 1 0
|
||||
github.com/muety/wakapi/models/user.go:91.39,93.2 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
|
||||
@ -121,78 +130,65 @@ github.com/muety/wakapi/models/filters.go:39.8,39.27 1 1
|
||||
github.com/muety/wakapi/models/filters.go:39.27,41.3 1 0
|
||||
github.com/muety/wakapi/models/filters.go:41.8,41.28 1 1
|
||||
github.com/muety/wakapi/models/filters.go:41.28,43.3 1 0
|
||||
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/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.52,16.2 1 0
|
||||
github.com/muety/wakapi/utils/common.go:18.40,20.2 1 0
|
||||
github.com/muety/wakapi/utils/common.go:22.44,24.2 1 0
|
||||
github.com/muety/wakapi/utils/common.go:26.45,28.2 1 0
|
||||
github.com/muety/wakapi/utils/common.go:30.24,32.2 1 0
|
||||
github.com/muety/wakapi/utils/common.go:34.56,37.45 3 1
|
||||
github.com/muety/wakapi/utils/common.go:40.2,40.40 1 1
|
||||
github.com/muety/wakapi/utils/common.go:37.45,39.3 1 1
|
||||
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/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.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:102.2,109.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:93.3,94.17 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:86.17,88.18 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:88.18,90.5 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:94.17,96.18 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:96.18,98.5 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:112.48,116.51 2 0
|
||||
github.com/muety/wakapi/utils/summary.go:119.2,119.12 1 0
|
||||
github.com/muety/wakapi/utils/summary.go:116.51,118.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/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
|
||||
@ -200,9 +196,9 @@ 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: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.54,41.3 1 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
|
||||
@ -230,28 +226,6 @@ 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/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/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,131 +234,242 @@ 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/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/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/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/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/config/config.go:113.70,115.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:117.65,119.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:121.82,131.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:133.31,135.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:137.32,139.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:141.74,142.19 1 0
|
||||
github.com/muety/wakapi/config/config.go:143.10,144.34 1 0
|
||||
github.com/muety/wakapi/config/config.go:144.34,153.4 8 0
|
||||
github.com/muety/wakapi/config/config.go:157.73,158.33 1 0
|
||||
github.com/muety/wakapi/config/config.go:158.33,166.17 5 0
|
||||
github.com/muety/wakapi/config/config.go:170.3,171.13 2 0
|
||||
github.com/muety/wakapi/config/config.go:166.17,168.4 1 0
|
||||
github.com/muety/wakapi/config/config.go:175.50,176.19 1 0
|
||||
github.com/muety/wakapi/config/config.go:189.2,189.12 1 0
|
||||
github.com/muety/wakapi/config/config.go:177.23,181.5 1 0
|
||||
github.com/muety/wakapi/config/config.go:182.26,185.5 1 0
|
||||
github.com/muety/wakapi/config/config.go:186.24,187.48 1 0
|
||||
github.com/muety/wakapi/config/config.go:192.53,203.2 1 1
|
||||
github.com/muety/wakapi/config/config.go:205.56,207.16 2 1
|
||||
github.com/muety/wakapi/config/config.go:211.2,218.3 1 1
|
||||
github.com/muety/wakapi/config/config.go:207.16,209.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:221.54,223.2 1 1
|
||||
github.com/muety/wakapi/config/config.go:225.60,227.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:229.59,231.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:233.57,235.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:237.53,239.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:241.29,243.2 1 1
|
||||
github.com/muety/wakapi/config/config.go:245.27,247.16 2 0
|
||||
github.com/muety/wakapi/config/config.go:250.2,253.16 3 0
|
||||
github.com/muety/wakapi/config/config.go:257.2,257.41 1 0
|
||||
github.com/muety/wakapi/config/config.go:247.16,249.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:253.16,255.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:260.48,272.16 3 0
|
||||
github.com/muety/wakapi/config/config.go:275.2,277.16 3 0
|
||||
github.com/muety/wakapi/config/config.go:281.2,281.55 1 0
|
||||
github.com/muety/wakapi/config/config.go:285.2,285.15 1 0
|
||||
github.com/muety/wakapi/config/config.go:272.16,274.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:277.16,279.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:281.55,283.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:288.38,289.43 1 0
|
||||
github.com/muety/wakapi/config/config.go:292.2,292.15 1 0
|
||||
github.com/muety/wakapi/config/config.go:289.43,291.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:295.45,296.27 1 0
|
||||
github.com/muety/wakapi/config/config.go:299.2,299.15 1 0
|
||||
github.com/muety/wakapi/config/config.go:296.27,298.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:302.50,306.91 1 0
|
||||
github.com/muety/wakapi/config/config.go:306.91,307.29 1 0
|
||||
github.com/muety/wakapi/config/config.go:311.4,314.48 3 0
|
||||
github.com/muety/wakapi/config/config.go:317.4,317.39 1 0
|
||||
github.com/muety/wakapi/config/config.go:320.4,320.69 1 0
|
||||
github.com/muety/wakapi/config/config.go:307.29,309.5 1 0
|
||||
github.com/muety/wakapi/config/config.go:314.48,316.5 1 0
|
||||
github.com/muety/wakapi/config/config.go:317.39,319.5 1 0
|
||||
github.com/muety/wakapi/config/config.go:322.79,326.27 2 0
|
||||
github.com/muety/wakapi/config/config.go:333.4,333.16 1 0
|
||||
github.com/muety/wakapi/config/config.go:326.27,327.84 1 0
|
||||
github.com/muety/wakapi/config/config.go:327.84,328.57 1 0
|
||||
github.com/muety/wakapi/config/config.go:328.57,330.7 1 0
|
||||
github.com/muety/wakapi/config/config.go:335.17,337.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:340.26,342.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:344.20,346.2 1 0
|
||||
github.com/muety/wakapi/config/config.go:348.21,353.96 3 0
|
||||
github.com/muety/wakapi/config/config.go:357.2,365.52 5 0
|
||||
github.com/muety/wakapi/config/config.go:369.2,369.47 1 0
|
||||
github.com/muety/wakapi/config/config.go:375.2,375.70 1 0
|
||||
github.com/muety/wakapi/config/config.go:379.2,379.28 1 0
|
||||
github.com/muety/wakapi/config/config.go:383.2,383.29 1 0
|
||||
github.com/muety/wakapi/config/config.go:388.2,389.14 2 0
|
||||
github.com/muety/wakapi/config/config.go:353.96,355.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:365.52,367.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:369.47,370.14 1 0
|
||||
github.com/muety/wakapi/config/config.go:370.14,372.4 1 0
|
||||
github.com/muety/wakapi/config/config.go:375.70,377.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:379.28,381.3 1 0
|
||||
github.com/muety/wakapi/config/config.go:383.29,386.3 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:19.91,25.2 1 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:27.90,30.2 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:32.90,35.2 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:37.71,38.71 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:38.71,40.3 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:43.107,47.16 3 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:51.2,51.31 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:67.2,68.12 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:47.16,49.3 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:51.31,52.31 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:57.3,57.29 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:64.3,64.9 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:52.31,55.4 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:57.29,60.4 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:60.9,63.4 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:71.70,72.39 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:77.2,77.14 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:72.39,73.60 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:73.60,75.4 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:80.92,82.16 2 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:86.2,89.16 4 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:92.2,92.18 1 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:82.16,84.3 1 1
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:89.16,91.3 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:95.92,97.16 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:101.2,102.16 2 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:109.2,109.18 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:97.16,99.3 1 0
|
||||
github.com/muety/wakapi/middlewares/authenticate.go:102.16,104.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:20.102,21.43 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:21.43,27.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:30.80,39.44 7 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:45.2,54.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:39.44,40.38 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:40.38,42.4 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:57.41,59.14 2 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:62.2,62.14 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:65.2,65.11 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:59.14,61.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:62.14,64.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:68.41,69.42 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:72.2,72.12 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:69.42,71.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:103.52,105.2 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:117.45,118.20 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:118.20,122.3 3 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:124.54,127.18 3 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:134.2,135.15 2 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:127.18,130.17 2 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:130.17,132.4 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:137.42,138.20 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:138.20,140.3 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:142.36,144.2 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:145.42,147.2 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:148.40,150.2 1 0
|
||||
github.com/muety/wakapi/middlewares/logging.go:151.52,153.2 1 0
|
||||
github.com/muety/wakapi/middlewares/principal.go:15.62,17.2 1 0
|
||||
github.com/muety/wakapi/middlewares/principal.go:19.58,21.2 1 0
|
||||
github.com/muety/wakapi/middlewares/principal.go:42.71,43.43 1 0
|
||||
github.com/muety/wakapi/middlewares/principal.go:43.43,45.3 1 0
|
||||
github.com/muety/wakapi/middlewares/principal.go:48.81,51.2 2 0
|
||||
github.com/muety/wakapi/middlewares/principal.go:53.55,54.52 1 0
|
||||
github.com/muety/wakapi/middlewares/principal.go:54.52,56.3 1 0
|
||||
github.com/muety/wakapi/middlewares/principal.go:59.49,60.52 1 0
|
||||
github.com/muety/wakapi/middlewares/principal.go:63.2,63.12 1 0
|
||||
github.com/muety/wakapi/middlewares/principal.go:60.52,62.3 1 0
|
||||
github.com/muety/wakapi/middlewares/sentry.go:14.60,15.43 1 0
|
||||
github.com/muety/wakapi/middlewares/sentry.go:15.43,19.3 1 0
|
||||
github.com/muety/wakapi/middlewares/sentry.go:22.78,25.54 3 0
|
||||
github.com/muety/wakapi/middlewares/sentry.go:25.54,26.43 1 0
|
||||
github.com/muety/wakapi/middlewares/sentry.go:26.43,28.4 1 0
|
||||
github.com/muety/wakapi/services/alias.go:17.77,22.2 1 1
|
||||
github.com/muety/wakapi/services/alias.go:26.60,27.43 1 1
|
||||
github.com/muety/wakapi/services/alias.go:30.2,30.14 1 1
|
||||
github.com/muety/wakapi/services/alias.go:27.43,29.3 1 1
|
||||
github.com/muety/wakapi/services/alias.go:33.62,35.16 2 1
|
||||
github.com/muety/wakapi/services/alias.go:38.2,38.12 1 1
|
||||
github.com/muety/wakapi/services/alias.go:35.16,37.3 1 1
|
||||
github.com/muety/wakapi/services/alias.go:41.76,43.16 2 0
|
||||
github.com/muety/wakapi/services/alias.go:46.2,46.21 1 0
|
||||
github.com/muety/wakapi/services/alias.go:43.16,45.3 1 0
|
||||
github.com/muety/wakapi/services/alias.go:49.113,51.16 2 0
|
||||
github.com/muety/wakapi/services/alias.go:54.2,54.21 1 0
|
||||
github.com/muety/wakapi/services/alias.go:51.16,53.3 1 0
|
||||
github.com/muety/wakapi/services/alias.go:57.108,58.32 1 1
|
||||
github.com/muety/wakapi/services/alias.go:64.2,65.46 2 1
|
||||
github.com/muety/wakapi/services/alias.go:70.2,70.19 1 1
|
||||
github.com/muety/wakapi/services/alias.go:58.32,59.52 1 1
|
||||
github.com/muety/wakapi/services/alias.go:59.52,61.4 1 1
|
||||
github.com/muety/wakapi/services/alias.go:65.46,66.48 1 1
|
||||
github.com/muety/wakapi/services/alias.go:66.48,68.4 1 1
|
||||
github.com/muety/wakapi/services/alias.go:73.77,75.16 2 0
|
||||
github.com/muety/wakapi/services/alias.go:78.2,79.20 2 0
|
||||
github.com/muety/wakapi/services/alias.go:75.16,77.3 1 0
|
||||
github.com/muety/wakapi/services/alias.go:82.60,83.24 1 0
|
||||
github.com/muety/wakapi/services/alias.go:86.2,88.12 3 0
|
||||
github.com/muety/wakapi/services/alias.go:83.24,85.3 1 0
|
||||
github.com/muety/wakapi/services/alias.go:91.69,94.28 3 0
|
||||
github.com/muety/wakapi/services/alias.go:102.2,104.31 2 0
|
||||
github.com/muety/wakapi/services/alias.go:108.2,108.12 1 0
|
||||
github.com/muety/wakapi/services/alias.go:94.28,95.21 1 0
|
||||
github.com/muety/wakapi/services/alias.go:98.3,99.16 2 0
|
||||
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/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/heartbeat.go:29.80,34.32 3 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:41.2,41.55 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:34.32,35.36 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:35.36,38.4 2 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:44.53,46.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:48.76,50.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:52.96,54.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:56.111,58.16 2 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:61.2,61.43 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:58.16,60.3 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:64.116,66.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:68.78,70.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:72.62,74.2 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:76.116,78.16 2 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:82.2,82.28 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:86.2,86.24 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:78.16,80.3 1 0
|
||||
github.com/muety/wakapi/services/heartbeat.go:82.28,84.3 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/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
|
||||
@ -397,19 +482,21 @@ 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/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,77.93 2 0
|
||||
github.com/muety/wakapi/services/user.go:83.2,83.38 1 0
|
||||
github.com/muety/wakapi/services/user.go:77.93,79.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:79.8,81.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:86.73,89.2 2 0
|
||||
github.com/muety/wakapi/services/user.go:91.78,95.2 3 0
|
||||
github.com/muety/wakapi/services/user.go:97.99,100.2 2 0
|
||||
github.com/muety/wakapi/services/user.go:102.106,105.96 3 0
|
||||
github.com/muety/wakapi/services/user.go:110.2,110.68 1 0
|
||||
github.com/muety/wakapi/services/user.go:105.96,107.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:107.8,109.3 1 0
|
||||
github.com/muety/wakapi/services/user.go:113.57,116.2 2 0
|
||||
github.com/muety/wakapi/services/user.go:118.38,120.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
|
||||
@ -450,74 +537,36 @@ github.com/muety/wakapi/services/aggregation.go:136.62,140.4 1 0
|
||||
github.com/muety/wakapi/services/aggregation.go:148.83,163.41 5 0
|
||||
github.com/muety/wakapi/services/aggregation.go:163.41,173.3 3 0
|
||||
github.com/muety/wakapi/services/aggregation.go:176.34,179.2 2 0
|
||||
github.com/muety/wakapi/services/alias.go:17.77,22.2 1 1
|
||||
github.com/muety/wakapi/services/alias.go:26.60,27.43 1 1
|
||||
github.com/muety/wakapi/services/alias.go:30.2,30.14 1 1
|
||||
github.com/muety/wakapi/services/alias.go:27.43,29.3 1 1
|
||||
github.com/muety/wakapi/services/alias.go:33.62,35.16 2 1
|
||||
github.com/muety/wakapi/services/alias.go:38.2,38.12 1 1
|
||||
github.com/muety/wakapi/services/alias.go:35.16,37.3 1 1
|
||||
github.com/muety/wakapi/services/alias.go:41.76,43.16 2 0
|
||||
github.com/muety/wakapi/services/alias.go:46.2,46.21 1 0
|
||||
github.com/muety/wakapi/services/alias.go:43.16,45.3 1 0
|
||||
github.com/muety/wakapi/services/alias.go:49.113,51.16 2 0
|
||||
github.com/muety/wakapi/services/alias.go:54.2,54.21 1 0
|
||||
github.com/muety/wakapi/services/alias.go:51.16,53.3 1 0
|
||||
github.com/muety/wakapi/services/alias.go:57.108,58.32 1 1
|
||||
github.com/muety/wakapi/services/alias.go:64.2,65.46 2 1
|
||||
github.com/muety/wakapi/services/alias.go:70.2,70.19 1 1
|
||||
github.com/muety/wakapi/services/alias.go:58.32,59.52 1 1
|
||||
github.com/muety/wakapi/services/alias.go:59.52,61.4 1 1
|
||||
github.com/muety/wakapi/services/alias.go:65.46,66.48 1 1
|
||||
github.com/muety/wakapi/services/alias.go:66.48,68.4 1 1
|
||||
github.com/muety/wakapi/services/alias.go:73.77,75.16 2 0
|
||||
github.com/muety/wakapi/services/alias.go:78.2,79.20 2 0
|
||||
github.com/muety/wakapi/services/alias.go:75.16,77.3 1 0
|
||||
github.com/muety/wakapi/services/alias.go:82.60,83.24 1 0
|
||||
github.com/muety/wakapi/services/alias.go:86.2,88.12 3 0
|
||||
github.com/muety/wakapi/services/alias.go:83.24,85.3 1 0
|
||||
github.com/muety/wakapi/services/alias.go:91.69,94.28 3 0
|
||||
github.com/muety/wakapi/services/alias.go:102.2,104.31 2 0
|
||||
github.com/muety/wakapi/services/alias.go:108.2,108.12 1 0
|
||||
github.com/muety/wakapi/services/alias.go:94.28,95.21 1 0
|
||||
github.com/muety/wakapi/services/alias.go:98.3,99.16 2 0
|
||||
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/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.151 1 0
|
||||
github.com/muety/wakapi/services/misc.go:91.3,91.48 1 0
|
||||
github.com/muety/wakapi/services/misc.go:82.151,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/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:39.136,42.66 2 1
|
||||
github.com/muety/wakapi/services/summary.go:47.2,47.44 1 1
|
||||
github.com/muety/wakapi/services/summary.go:53.2,53.65 1 1
|
||||
github.com/muety/wakapi/services/summary.go:58.2,59.16 2 1
|
||||
github.com/muety/wakapi/services/summary.go:64.2,66.30 3 1
|
||||
github.com/muety/wakapi/services/summary.go:42.52,44.3 1 0
|
||||
github.com/muety/wakapi/services/summary.go:42.66,44.3 1 0
|
||||
github.com/muety/wakapi/services/summary.go:47.44,50.3 2 1
|
||||
github.com/muety/wakapi/services/summary.go:53.65,55.3 1 0
|
||||
github.com/muety/wakapi/services/summary.go:59.16,61.3 1 0
|
||||
@ -600,3 +649,10 @@ 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/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
|
||||
|
@ -49,6 +49,7 @@
|
||||
"DataWeave": "#003a52",
|
||||
"DM": "#447265",
|
||||
"Dockerfile": "#384d54",
|
||||
"Docker": "#384d54",
|
||||
"Dogescript": "#cca760",
|
||||
"Dylan": "#6c616e",
|
||||
"E": "#ccce35",
|
||||
@ -209,6 +210,7 @@
|
||||
"Stan": "#b2011d",
|
||||
"Standard ML": "#dc566d",
|
||||
"SuperCollider": "#46390b",
|
||||
"Svelte": "#ff3e00",
|
||||
"Swift": "#ffac45",
|
||||
"SystemVerilog": "#DAE1C2",
|
||||
"Tcl": "#e4cc98",
|
||||
|
1
go.mod
1
go.mod
@ -5,6 +5,7 @@ go 1.13
|
||||
require (
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
||||
github.com/emvi/logbuch v1.1.1
|
||||
github.com/getsentry/sentry-go v0.10.0
|
||||
github.com/go-co-op/gocron v0.3.3
|
||||
github.com/go-openapi/spec v0.20.2 // indirect
|
||||
github.com/gorilla/handlers v1.4.2
|
||||
|
85
go.sum
85
go.sum
@ -1,7 +1,11 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
|
||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
@ -9,10 +13,12 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@ -28,6 +34,7 @@ github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6l
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
@ -42,6 +49,7 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
|
||||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
@ -59,31 +67,46 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/emvi/logbuch v1.1.1 h1:poBGNbHy/nB95oNoqLKAaJoBrcKxTO0W9DhMijKEkkU=
|
||||
github.com/emvi/logbuch v1.1.1/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw=
|
||||
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/getsentry/sentry-go v0.10.0 h1:6gwY+66NHKqyZrdi6O2jGdo7wGdo9b3B69E01NFgT5g=
|
||||
github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||
github.com/go-co-op/gocron v0.3.3 h1:QnarcMZWWKrEP25uCbtDiLsnnGw+PhCjL3wNITdWJOs=
|
||||
github.com/go-co-op/gocron v0.3.3/go.mod h1:Y9PWlYqDChf2Nbgg7kfS+ZsXHDTZbMZYPEQ0MILqH+M=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
|
||||
github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
@ -118,6 +141,9 @@ github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4
|
||||
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
|
||||
github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o=
|
||||
github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
@ -134,12 +160,14 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@ -156,6 +184,7 @@ github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlI
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
@ -182,8 +211,14 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
|
||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
|
||||
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
|
||||
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
|
||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
||||
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
@ -251,11 +286,21 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
|
||||
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
|
||||
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
||||
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
|
||||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@ -266,6 +311,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
@ -301,7 +348,10 @@ github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO
|
||||
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@ -317,6 +367,7 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
@ -335,8 +386,10 @@ github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2f
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
|
||||
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||
@ -355,6 +408,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -395,10 +449,13 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
|
||||
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
@ -438,14 +495,29 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E=
|
||||
github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||
@ -479,6 +551,7 @@ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
@ -504,12 +577,15 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
|
||||
@ -543,6 +619,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@ -570,10 +647,12 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
@ -625,9 +704,13 @@ gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw=
|
||||
gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
@ -635,12 +718,14 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
|
||||
|
28
main.go
28
main.go
@ -117,9 +117,7 @@ func main() {
|
||||
defer sqlDb.Close()
|
||||
|
||||
// Migrate database schema
|
||||
migrations.RunPreMigrations(db, config)
|
||||
runDatabaseMigrations()
|
||||
migrations.RunCustomPostMigrations(db, config)
|
||||
migrations.Run(db, config)
|
||||
|
||||
// Repositories
|
||||
aliasRepository = repositories.NewAliasRepository(db)
|
||||
@ -149,6 +147,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)
|
||||
@ -166,17 +165,15 @@ func main() {
|
||||
// Setup Routers
|
||||
router := mux.NewRouter()
|
||||
rootRouter := router.PathPrefix("/").Subrouter()
|
||||
apiRouter := router.PathPrefix("/api").Subrouter()
|
||||
apiRouter := router.PathPrefix("/api").Subrouter().StrictSlash(true)
|
||||
|
||||
// Globally used middlewares
|
||||
recoveryMiddleware := handlers.RecoveryHandler()
|
||||
loggingMiddleware := middlewares.NewLoggingMiddleware(
|
||||
log.New(os.Stdout, "", log.LstdFlags),
|
||||
[]string{"/assets"},
|
||||
)
|
||||
|
||||
// Router configs
|
||||
router.Use(loggingMiddleware, recoveryMiddleware)
|
||||
router.Use(middlewares.NewPrincipalMiddleware())
|
||||
router.Use(middlewares.NewLoggingMiddleware(logbuch.Info, []string{"/assets"}))
|
||||
router.Use(handlers.RecoveryHandler())
|
||||
if config.Sentry.Dsn != "" {
|
||||
router.Use(middlewares.NewSentryMiddleware())
|
||||
}
|
||||
|
||||
// Route registrations
|
||||
homeHandler.RegisterRoutes(rootRouter)
|
||||
@ -189,6 +186,7 @@ func main() {
|
||||
summaryApiHandler.RegisterRoutes(apiRouter)
|
||||
healthApiHandler.RegisterRoutes(apiRouter)
|
||||
heartbeatApiHandler.RegisterRoutes(apiRouter)
|
||||
metricsHandler.RegisterRoutes(apiRouter)
|
||||
wakatimeV1AllHandler.RegisterRoutes(apiRouter)
|
||||
wakatimeV1SummariesHandler.RegisterRoutes(apiRouter)
|
||||
wakatimeV1StatsHandler.RegisterRoutes(apiRouter)
|
||||
@ -269,9 +267,3 @@ func listen(handler http.Handler) {
|
||||
|
||||
<-make(chan interface{}, 1)
|
||||
}
|
||||
|
||||
func runDatabaseMigrations() {
|
||||
if err := config.GetMigrationFunc(config.Db.Dialect)(db); err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/services"
|
||||
@ -15,6 +13,7 @@ type AuthenticateMiddleware struct {
|
||||
config *conf.Config
|
||||
userSrvc services.IUserService
|
||||
optionalForPaths []string
|
||||
redirectTarget string // optional
|
||||
}
|
||||
|
||||
func NewAuthenticateMiddleware(userService services.IUserService) *AuthenticateMiddleware {
|
||||
@ -30,6 +29,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,17 +54,18 @@ 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
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), models.UserKey, user)
|
||||
next(w, r.WithContext(ctx))
|
||||
SetPrincipal(r, user)
|
||||
next(w, r)
|
||||
}
|
||||
|
||||
func (m *AuthenticateMiddleware) isOptional(requestPath string) bool {
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/middlewares"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@ -39,7 +39,7 @@ func (m *WakatimeRelayMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
if user == nil || user.WakatimeApiKey == "" {
|
||||
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,15 +42,15 @@ 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",
|
||||
ww.Status(),
|
||||
r.Method,
|
||||
r.URL.String(),
|
||||
duration,
|
||||
ww.BytesWritten(),
|
||||
readUserIP(r),
|
||||
readUserID(r),
|
||||
)
|
||||
}
|
||||
|
||||
@ -64,6 +65,13 @@ func readUserIP(r *http.Request) string {
|
||||
return ip
|
||||
}
|
||||
|
||||
func readUserID(r *http.Request) string {
|
||||
if user := GetPrincipal(r); user != nil {
|
||||
return user.ID
|
||||
}
|
||||
return "-"
|
||||
}
|
||||
|
||||
// The below writer-wrapping code has been lifted from
|
||||
// https://github.com/zenazn/goji/blob/master/web/middleware/logger.go - because
|
||||
// it does exactly what is needed, and it's unlikely to change in any
|
||||
|
64
middlewares/principal.go
Normal file
64
middlewares/principal.go
Normal file
@ -0,0 +1,64 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/muety/wakapi/models"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const keyPrincipal = "principal"
|
||||
|
||||
type PrincipalContainer struct {
|
||||
principal *models.User
|
||||
}
|
||||
|
||||
func (c *PrincipalContainer) SetPrincipal(user *models.User) {
|
||||
c.principal = user
|
||||
}
|
||||
|
||||
func (c *PrincipalContainer) GetPrincipal() *models.User {
|
||||
return c.principal
|
||||
}
|
||||
|
||||
// This middleware is a bit of a dirty workaround to the fact that a http.Request's context
|
||||
// does not allow to pass values from an inner to an outer middleware. Calling WithContext() on a
|
||||
// request shallow-copies the whole request itself and therefore, in a chain of handler1(handler2()),
|
||||
// handler 1 will not have access to values handler 2 writes to its context. In addition, Context.WithValue
|
||||
// returns a new context with the old context as a parent.
|
||||
//
|
||||
// As a concrete example, SentryMiddleware as well as LoggingMiddleware should be quite the outer layers,
|
||||
// while AuthenticationMiddleware is on the very inside of the chain. However, we still want sentry or the
|
||||
// logger to have access to the user object populated by the auth. middleware, if present.
|
||||
//
|
||||
// This middleware shall be included as the outermost layers and it injects a stateful container that does
|
||||
// nothing but conditionally hold a reference to an authenticated user object.
|
||||
//
|
||||
// Other reference: https://stackoverflow.com/questions/55972869/send-errors-to-sentry-with-golang-and-mux
|
||||
|
||||
type PrincipalMiddleware struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func NewPrincipalMiddleware() func(handler http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
return &PrincipalMiddleware{handler: h}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PrincipalMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.WithValue(r.Context(), keyPrincipal, &PrincipalContainer{})
|
||||
p.handler.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
|
||||
func SetPrincipal(r *http.Request, user *models.User) {
|
||||
if p := r.Context().Value(keyPrincipal); p != nil {
|
||||
p.(*PrincipalContainer).SetPrincipal(user)
|
||||
}
|
||||
}
|
||||
|
||||
func GetPrincipal(r *http.Request) *models.User {
|
||||
if p := r.Context().Value(keyPrincipal); p != nil {
|
||||
return p.(*PrincipalContainer).GetPrincipal()
|
||||
}
|
||||
return nil
|
||||
}
|
30
middlewares/sentry.go
Normal file
30
middlewares/sentry.go
Normal file
@ -0,0 +1,30 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/getsentry/sentry-go"
|
||||
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type SentryMiddleware struct {
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func NewSentryMiddleware() func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
return sentryhttp.New(sentryhttp.Options{
|
||||
Repanic: true,
|
||||
}).Handle(&SentryMiddleware{handler: h})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SentryMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.WithValue(r.Context(), "-", "-")
|
||||
h.handler.ServeHTTP(w, r.WithContext(ctx))
|
||||
if hub := sentry.GetHubFromContext(ctx); hub != nil {
|
||||
if user := GetPrincipal(r); user != nil {
|
||||
hub.Scope().SetUser(sentry.User{ID: user.ID})
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
41
migrations/20210221_add_created_date_column.go
Normal file
41
migrations/20210221_add_created_date_column.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 = "20210221-add_created_date_column"
|
||||
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 heartbeats SET created_at = time 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)
|
||||
}
|
@ -28,9 +28,17 @@ func registerPostMigration(f migrationFunc) {
|
||||
postMigrations = append(postMigrations, f)
|
||||
}
|
||||
|
||||
// NOTE: Currently, migrations themselves keep track
|
||||
// of whether they have run, yet or not, because some
|
||||
// simply run on every start.
|
||||
func Run(db *gorm.DB, cfg *config.Config) {
|
||||
RunPreMigrations(db, cfg)
|
||||
RunSchemaMigrations(db, cfg)
|
||||
RunPostMigrations(db, cfg)
|
||||
}
|
||||
|
||||
func RunSchemaMigrations(db *gorm.DB, cfg *config.Config) {
|
||||
if err := cfg.GetMigrationFunc(cfg.Db.Dialect)(db); err != nil {
|
||||
logbuch.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func RunPreMigrations(db *gorm.DB, cfg *config.Config) {
|
||||
sort.Sort(preMigrations)
|
||||
@ -43,7 +51,7 @@ func RunPreMigrations(db *gorm.DB, cfg *config.Config) {
|
||||
}
|
||||
}
|
||||
|
||||
func RunCustomPostMigrations(db *gorm.DB, cfg *config.Config) {
|
||||
func RunPostMigrations(db *gorm.DB, cfg *config.Config) {
|
||||
sort.Sort(postMigrations)
|
||||
|
||||
for _, m := range postMigrations {
|
||||
|
@ -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"`
|
||||
}
|
@ -4,29 +4,29 @@ import (
|
||||
"fmt"
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Heartbeat struct {
|
||||
ID uint `gorm:"primary_key" hash:"ignore"`
|
||||
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" hash:"ignore"`
|
||||
UserID string `json:"-" gorm:"not null; index:idx_time_user"`
|
||||
Entity string `json:"entity" gorm:"not null; index:idx_entity"`
|
||||
Type string `json:"type"`
|
||||
Category string `json:"category"`
|
||||
Project string `json:"project"`
|
||||
Branch string `json:"branch"`
|
||||
Language string `json:"language" gorm:"index:idx_language"`
|
||||
IsWrite bool `json:"is_write"`
|
||||
Editor string `json:"editor" hash:"ignore"` // ignored because editor might be parsed differently by wakatime
|
||||
OperatingSystem string `json:"operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
|
||||
Machine string `json:"machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
|
||||
Time CustomTime `json:"time" gorm:"type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time,idx_time_user" swaggertype:"primitive,number"`
|
||||
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
|
||||
Origin string `json:"-" hash:"ignore"`
|
||||
OriginId string `json:"-" hash:"ignore"`
|
||||
languageRegex *regexp.Regexp `hash:"ignore"`
|
||||
ID uint `gorm:"primary_key" hash:"ignore"`
|
||||
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" hash:"ignore"`
|
||||
UserID string `json:"-" gorm:"not null; index:idx_time_user"`
|
||||
Entity string `json:"entity" gorm:"not null; index:idx_entity"`
|
||||
Type string `json:"type"`
|
||||
Category string `json:"category"`
|
||||
Project string `json:"project"`
|
||||
Branch string `json:"branch"`
|
||||
Language string `json:"language" gorm:"index:idx_language"`
|
||||
IsWrite bool `json:"is_write"`
|
||||
Editor string `json:"editor" hash:"ignore"` // ignored because editor might be parsed differently by wakatime
|
||||
OperatingSystem string `json:"operating_system" hash:"ignore"` // ignored because os might be parsed differently by wakatime
|
||||
Machine string `json:"machine" hash:"ignore"` // ignored because wakatime api doesn't return machines currently
|
||||
Time CustomTime `json:"time" gorm:"type:timestamp; index:idx_time,idx_time_user" swaggertype:"primitive,number"`
|
||||
Hash string `json:"-" gorm:"type:varchar(17); uniqueIndex"`
|
||||
Origin string `json:"-" hash:"ignore"`
|
||||
OriginId string `json:"-" hash:"ignore"`
|
||||
CreatedAt CustomTime `json:"created_at" gorm:"type:timestamp" swaggertype:"primitive,number" hash:"ignore"` // https://gorm.io/docs/conventions.html#CreatedAt
|
||||
}
|
||||
|
||||
func (h *Heartbeat) Valid() bool {
|
||||
@ -34,18 +34,12 @@ func (h *Heartbeat) Valid() bool {
|
||||
}
|
||||
|
||||
func (h *Heartbeat) Augment(languageMappings map[string]string) {
|
||||
if h.languageRegex == nil {
|
||||
h.languageRegex = regexp.MustCompile(`^.+\.(.+)$`)
|
||||
for ending, value := range languageMappings {
|
||||
if strings.HasSuffix(h.Entity, "."+ending) {
|
||||
h.Language = value
|
||||
return
|
||||
}
|
||||
}
|
||||
groups := h.languageRegex.FindAllStringSubmatch(h.Entity, -1)
|
||||
if len(groups) == 0 || len(groups[0]) != 2 {
|
||||
return
|
||||
}
|
||||
ending := groups[0][1]
|
||||
if _, ok := languageMappings[ending]; !ok {
|
||||
return
|
||||
}
|
||||
h.Language, _ = languageMappings[ending]
|
||||
}
|
||||
|
||||
func (h *Heartbeat) GetKey(t uint8) (key string) {
|
||||
|
@ -26,17 +26,24 @@ func TestHeartbeat_Valid_MissingUser(t *testing.T) {
|
||||
|
||||
func TestHeartbeat_Augment(t *testing.T) {
|
||||
testMappings := map[string]string{
|
||||
"py": "Python3",
|
||||
"py": "Python3",
|
||||
"foo": "Foo Script",
|
||||
"blade.php": "Blade",
|
||||
}
|
||||
|
||||
sut := &Heartbeat{
|
||||
sut1, sut2 := &Heartbeat{
|
||||
Entity: "~/dev/file.py",
|
||||
Language: "Python",
|
||||
}, &Heartbeat{
|
||||
Entity: "~/dev/file.blade.php",
|
||||
Language: "unknown",
|
||||
}
|
||||
|
||||
sut.Augment(testMappings)
|
||||
sut1.Augment(testMappings)
|
||||
sut2.Augment(testMappings)
|
||||
|
||||
assert.Equal(t, "Python3", sut.Language)
|
||||
assert.Equal(t, "Python3", sut1.Language)
|
||||
assert.Equal(t, "Blade", sut2.Language)
|
||||
}
|
||||
|
||||
func TestHeartbeat_GetKey(t *testing.T) {
|
||||
|
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
|
||||
}
|
@ -35,7 +35,7 @@ type SummaryItem struct {
|
||||
ID uint `json:"-" gorm:"primary_key"`
|
||||
Summary *Summary `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
SummaryID uint `json:"-"`
|
||||
Type uint8 `json:"-"`
|
||||
Type uint8 `json:"-" gorm:"index:idx_type"`
|
||||
Key string `json:"key"`
|
||||
Total time.Duration `json:"total" swaggertype:"primitive,integer"`
|
||||
}
|
||||
@ -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"
|
||||
|
@ -1,8 +1,23 @@
|
||||
package models
|
||||
|
||||
import "regexp"
|
||||
|
||||
const (
|
||||
MailPattern = "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+"
|
||||
)
|
||||
|
||||
var (
|
||||
mailRegex *regexp.Regexp
|
||||
)
|
||||
|
||||
func init() {
|
||||
mailRegex = regexp.MustCompile(MailPattern)
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id" gorm:"primary_key"`
|
||||
ApiKey string `json:"api_key" gorm:"unique"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"-"`
|
||||
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||
@ -12,6 +27,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:"-"`
|
||||
}
|
||||
|
||||
@ -22,6 +39,7 @@ type Login struct {
|
||||
|
||||
type Signup struct {
|
||||
Username string `schema:"username"`
|
||||
Email string `schema:"email"`
|
||||
Password string `schema:"password"`
|
||||
PasswordRepeat string `schema:"password_repeat"`
|
||||
}
|
||||
@ -32,26 +50,44 @@ type CredentialsReset struct {
|
||||
PasswordRepeat string `schema:"password_repeat"`
|
||||
}
|
||||
|
||||
type UserDataUpdate struct {
|
||||
Email string `schema:"email"`
|
||||
}
|
||||
|
||||
type TimeByUser struct {
|
||||
User string
|
||||
Time CustomTime
|
||||
}
|
||||
|
||||
type CountByUser struct {
|
||||
User string
|
||||
Count int64
|
||||
}
|
||||
|
||||
func (c *CredentialsReset) IsValid() bool {
|
||||
return validatePassword(c.PasswordNew) &&
|
||||
return ValidatePassword(c.PasswordNew) &&
|
||||
c.PasswordNew == c.PasswordRepeat
|
||||
}
|
||||
|
||||
func (s *Signup) IsValid() bool {
|
||||
return validateUsername(s.Username) &&
|
||||
validatePassword(s.Password) &&
|
||||
return ValidateUsername(s.Username) &&
|
||||
ValidateEmail(s.Email) &&
|
||||
ValidatePassword(s.Password) &&
|
||||
s.Password == s.PasswordRepeat
|
||||
}
|
||||
|
||||
func validateUsername(username string) bool {
|
||||
func (r *UserDataUpdate) IsValid() bool {
|
||||
return ValidateEmail(r.Email)
|
||||
}
|
||||
|
||||
func ValidateUsername(username string) bool {
|
||||
return len(username) >= 1 && username != "current"
|
||||
}
|
||||
|
||||
func validatePassword(password string) bool {
|
||||
func ValidatePassword(password string) bool {
|
||||
return len(password) >= 6
|
||||
}
|
||||
|
||||
func ValidateEmail(email string) bool {
|
||||
return email == "" || mailRegex.Match([]byte(email))
|
||||
}
|
||||
|
@ -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 {
|
||||
|
346
postman/Wakapi.postman_collection.json
Normal file
346
postman/Wakapi.postman_collection.json
Normal file
@ -0,0 +1,346 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "3dcc346d-a9a8-4699-8a52-459eb978b382",
|
||||
"name": "Wakapi",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Misc",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get health",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}/api/health",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"health"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get metrics",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Basic {{TOKEN}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}/api/metrics",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"metrics"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Heartbeats",
|
||||
"item": [
|
||||
{
|
||||
"name": "Create heartbeat",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Basic {{TOKEN}}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "X-Machine-Name",
|
||||
"value": "devmachine",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "User-Agent",
|
||||
"value": "wakatime/13.0.7 (Linux-4.15.0-91-generic-x86_64-with-glibc2.4) Python3.8.0.final.0 generator/1.42.1 generator-wakatime/4.0.0",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "[{\n \"entity\": \"/home/user1/dev/proejct1/main.go\",\n \"project\": \"Project 1\",\n \"language\": \"Go\",\n \"is_write\": true,\n \"type\": \"file\",\n \"category\": null,\n \"branch\": null,\n \"time\": 1616680499.113417\n}]",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}/api/heartbeat",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"heartbeat"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Summary",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get summary",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Basic {{TOKEN}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}/api/summary?interval=last_7_days",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"summary"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "interval",
|
||||
"value": "last_7_days"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Shields",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get Shields data",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Basic {{TOKEN}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}/api/compat/shields/v1/n1try/interval:today/language:Go",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"compat",
|
||||
"shields",
|
||||
"v1",
|
||||
"n1try",
|
||||
"interval:today",
|
||||
"language:Go"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "WakaTime",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get all time",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Basic {{TOKEN}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}/api/compat/wakatime/v1/users/current/all_time_since_today",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"compat",
|
||||
"wakatime",
|
||||
"v1",
|
||||
"users",
|
||||
"current",
|
||||
"all_time_since_today"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get stats",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Basic {{TOKEN}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}/api/compat/wakatime/v1/users/current/stats",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"compat",
|
||||
"wakatime",
|
||||
"v1",
|
||||
"users",
|
||||
"current",
|
||||
"stats"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get stats with range",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Basic {{TOKEN}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}/api/compat/wakatime/v1/users/current/stats/last_7_days",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"compat",
|
||||
"wakatime",
|
||||
"v1",
|
||||
"users",
|
||||
"current",
|
||||
"stats",
|
||||
"last_7_days"
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get summaries",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Basic {{TOKEN}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{BASE_URL}}/api/compat/wakatime/v1/users/current/summaries?start=2020-03-01T15:04:05Z&end=2020-03-31T15:04:05Z",
|
||||
"host": [
|
||||
"{{BASE_URL}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"compat",
|
||||
"wakatime",
|
||||
"v1",
|
||||
"users",
|
||||
"current",
|
||||
"summaries"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "start",
|
||||
"value": "2020-03-01T15:04:05Z"
|
||||
},
|
||||
{
|
||||
"key": "end",
|
||||
"value": "2020-03-31T15:04:05Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const apiKey = pm.variables.get('API_KEY')",
|
||||
"",
|
||||
"if (!apiKey) {",
|
||||
" throw new Error('no api key given')",
|
||||
"}",
|
||||
"",
|
||||
"const token = base64encode(apiKey)",
|
||||
"pm.variables.set('TOKEN', token)",
|
||||
"",
|
||||
"function base64encode(str) {",
|
||||
" return Buffer.from(str, 'utf-8').toString('base64')",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
""
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "BASE_URL",
|
||||
"value": "http://localhost:3000"
|
||||
},
|
||||
{
|
||||
"key": "API_KEY",
|
||||
"value": ""
|
||||
}
|
||||
]
|
||||
}
|
@ -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 {
|
||||
@ -57,6 +109,7 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
|
||||
updateMap := map[string]interface{}{
|
||||
"api_key": user.ApiKey,
|
||||
"password": user.Password,
|
||||
"email": user.Email,
|
||||
"last_logged_in_at": user.LastLoggedInAt,
|
||||
"share_data_max_days": user.ShareDataMaxDays,
|
||||
"share_editors": user.ShareEditors,
|
||||
@ -65,6 +118,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)
|
||||
|
@ -53,7 +53,7 @@ func (h *HeartbeatApiHandler) RegisterRoutes(router *mux.Router) {
|
||||
// @Router /heartbeat [post]
|
||||
func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
||||
var heartbeats []*models.Heartbeat
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
opSys, editor, _ := utils.ParseUserAgent(r.Header.Get("User-Agent"))
|
||||
machineName := r.Header.Get("X-Machine-Name")
|
||||
|
||||
@ -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 := middlewares.GetPrincipal(r)
|
||||
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, false)
|
||||
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, false)
|
||||
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
|
||||
}
|
@ -1,15 +1,16 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
v1 "github.com/muety/wakapi/models/compat/shields/v1"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"github.com/patrickmn/go-cache"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -22,12 +23,14 @@ type BadgeHandler struct {
|
||||
config *conf.Config
|
||||
userSrvc services.IUserService
|
||||
summarySrvc services.ISummaryService
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
func NewBadgeHandler(summaryService services.ISummaryService, userService services.IUserService) *BadgeHandler {
|
||||
return &BadgeHandler{
|
||||
summarySrvc: summaryService,
|
||||
userSrvc: userService,
|
||||
cache: cache.New(time.Hour, time.Hour),
|
||||
config: conf.Get(),
|
||||
}
|
||||
}
|
||||
@ -52,11 +55,6 @@ func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
intervalReg := regexp.MustCompile(intervalPattern)
|
||||
entityFilterReg := regexp.MustCompile(entityFilterPattern)
|
||||
|
||||
if userAgent := r.Header.Get("user-agent"); !strings.HasPrefix(userAgent, "Shields.io/") && !h.config.IsDev() {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
var filterEntity, filterKey string
|
||||
if groups := entityFilterReg.FindStringSubmatch(r.URL.Path); len(groups) > 2 {
|
||||
filterEntity, filterKey = groups[1], groups[2]
|
||||
@ -101,6 +99,12 @@ func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
filters = &models.Filters{}
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("%s_%v_%s_%s", user.ID, *interval, filterEntity, filterKey)
|
||||
if cacheResult, ok := h.cache.Get(cacheKey); ok {
|
||||
utils.RespondJSON(w, http.StatusOK, cacheResult.(*v1.BadgeData))
|
||||
return
|
||||
}
|
||||
|
||||
summary, err, status := h.loadUserSummary(user, interval)
|
||||
if err != nil {
|
||||
w.WriteHeader(status)
|
||||
@ -109,6 +113,7 @@ func (h *BadgeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
vm := v1.NewBadgeDataFrom(summary, filters)
|
||||
h.cache.SetDefault(cacheKey, vm)
|
||||
utils.RespondJSON(w, http.StatusOK, vm)
|
||||
}
|
||||
|
||||
@ -129,7 +134,7 @@ func (h *BadgeHandler) loadUserSummary(user *models.User, interval *models.Inter
|
||||
retrieveSummary = h.summarySrvc.Summarize
|
||||
}
|
||||
|
||||
summary, err := h.summarySrvc.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary)
|
||||
summary, err := h.summarySrvc.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, summaryParams.Recompute)
|
||||
if err != nil {
|
||||
return nil, err, http.StatusInternalServerError
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ func (h *AllTimeHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
values, _ := url.ParseQuery(r.URL.RawQuery)
|
||||
|
||||
requestedUser := vars["user"]
|
||||
authorizedUser := r.Context().Value(models.UserKey).(*models.User)
|
||||
authorizedUser := middlewares.GetPrincipal(r)
|
||||
|
||||
if requestedUser != authorizedUser.ID && requestedUser != "current" {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
@ -80,7 +80,7 @@ func (h *AllTimeHandler) loadUserSummary(user *models.User) (*models.Summary, er
|
||||
retrieveSummary = h.summarySrvc.Summarize
|
||||
}
|
||||
|
||||
summary, err := h.summarySrvc.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary)
|
||||
summary, err := h.summarySrvc.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, summaryParams.Recompute)
|
||||
if err != nil {
|
||||
return nil, err, http.StatusInternalServerError
|
||||
}
|
||||
|
@ -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)
|
||||
@ -41,10 +45,7 @@ func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
var vars = mux.Vars(r)
|
||||
var authorizedUser, requestedUser *models.User
|
||||
|
||||
if u := r.Context().Value(models.UserKey); u != nil {
|
||||
authorizedUser = u.(*models.User)
|
||||
}
|
||||
|
||||
authorizedUser = middlewares.GetPrincipal(r)
|
||||
if authorizedUser != nil && vars["user"] == "current" {
|
||||
vars["user"] = authorizedUser.ID
|
||||
}
|
||||
@ -56,7 +57,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"))
|
||||
@ -108,7 +114,7 @@ func (h *StatsHandler) loadUserSummary(user *models.User, start, end time.Time)
|
||||
Recompute: false,
|
||||
}
|
||||
|
||||
summary, err := h.summarySrvc.Aliased(overallParams.From, overallParams.To, user, h.summarySrvc.Retrieve)
|
||||
summary, err := h.summarySrvc.Aliased(overallParams.From, overallParams.To, user, h.summarySrvc.Retrieve, false)
|
||||
if err != nil {
|
||||
return nil, err, http.StatusInternalServerError
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ func (h *SummariesHandler) RegisterRoutes(router *mux.Router) {
|
||||
func (h *SummariesHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
requestedUser := vars["user"]
|
||||
authorizedUser := r.Context().Value(models.UserKey).(*models.User)
|
||||
authorizedUser := middlewares.GetPrincipal(r)
|
||||
|
||||
if requestedUser != authorizedUser.ID && requestedUser != "current" {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
@ -80,7 +80,7 @@ func (h *SummariesHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary, error, int) {
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
params := r.URL.Query()
|
||||
rangeParam, startParam, endParam := params.Get("range"), params.Get("start"), params.Get("end")
|
||||
|
||||
@ -121,7 +121,7 @@ func (h *SummariesHandler) loadUserSummaries(r *http.Request) ([]*models.Summary
|
||||
summaries := make([]*models.Summary, len(intervals))
|
||||
|
||||
for i, interval := range intervals {
|
||||
summary, err := h.summarySrvc.Aliased(interval[0], interval[1], user, h.summarySrvc.Retrieve)
|
||||
summary, err := h.summarySrvc.Aliased(interval[0], interval[1], user, h.summarySrvc.Retrieve, false)
|
||||
if err != nil {
|
||||
return nil, err, http.StatusInternalServerError
|
||||
}
|
||||
|
@ -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,20 @@ 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,
|
||||
"isDev": func() bool {
|
||||
return config.Get().IsDev()
|
||||
},
|
||||
"getBasePath": func() string {
|
||||
return config.Get().Server.BasePath
|
||||
},
|
||||
@ -102,3 +107,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)
|
||||
@ -117,6 +117,8 @@ func (h *SettingsHandler) dispatchAction(action string) action {
|
||||
switch action {
|
||||
case "change_password":
|
||||
return h.actionChangePassword
|
||||
case "update_user":
|
||||
return h.actionUpdateUser
|
||||
case "reset_apikey":
|
||||
return h.actionResetApiKey
|
||||
case "delete_alias":
|
||||
@ -141,12 +143,40 @@ func (h *SettingsHandler) dispatchAction(action string) action {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *SettingsHandler) actionUpdateUser(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
||||
if h.config.IsDev() {
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
user := middlewares.GetPrincipal(r)
|
||||
|
||||
var payload models.UserDataUpdate
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return http.StatusBadRequest, "", "missing parameters"
|
||||
}
|
||||
if err := credentialsDecoder.Decode(&payload, r.PostForm); err != nil {
|
||||
return http.StatusBadRequest, "", "missing parameters"
|
||||
}
|
||||
|
||||
if !payload.IsValid() {
|
||||
return http.StatusBadRequest, "", "invalid parameters"
|
||||
}
|
||||
|
||||
user.Email = payload.Email
|
||||
|
||||
if _, err := h.userSrvc.Update(user); err != nil {
|
||||
return http.StatusInternalServerError, "", conf.ErrInternalServerError
|
||||
}
|
||||
|
||||
return http.StatusOK, "user updated successfully", ""
|
||||
}
|
||||
|
||||
func (h *SettingsHandler) actionChangePassword(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
||||
if h.config.IsDev() {
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
|
||||
var credentials models.CredentialsReset
|
||||
if err := r.ParseForm(); err != nil {
|
||||
@ -166,13 +196,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 +211,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, "/"))
|
||||
@ -193,9 +223,9 @@ func (h *SettingsHandler) actionResetApiKey(w http.ResponseWriter, r *http.Reque
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
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)
|
||||
@ -208,7 +238,7 @@ func (h *SettingsHandler) actionUpdateSharing(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
var err error
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
|
||||
defer h.userSrvc.FlushCache()
|
||||
|
||||
@ -235,7 +265,7 @@ func (h *SettingsHandler) actionDeleteAlias(w http.ResponseWriter, r *http.Reque
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
aliasKey := r.PostFormValue("key")
|
||||
aliasType, err := strconv.Atoi(r.PostFormValue("type"))
|
||||
if err != nil {
|
||||
@ -255,7 +285,7 @@ func (h *SettingsHandler) actionAddAlias(w http.ResponseWriter, r *http.Request)
|
||||
if h.config.IsDev() {
|
||||
loadTemplates()
|
||||
}
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
aliasKey := r.PostFormValue("key")
|
||||
aliasValue := r.PostFormValue("value")
|
||||
aliasType, err := strconv.Atoi(r.PostFormValue("type"))
|
||||
@ -283,19 +313,20 @@ func (h *SettingsHandler) actionDeleteLanguageMapping(w http.ResponseWriter, r *
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
id, err := strconv.Atoi(r.PostFormValue("mapping_id"))
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, "", "could not delete mapping"
|
||||
}
|
||||
|
||||
if mapping, err := h.languageMappingSrvc.GetById(uint(id)); err != nil || mapping == nil {
|
||||
mapping, err := h.languageMappingSrvc.GetById(uint(id))
|
||||
if err != nil || mapping == nil {
|
||||
return http.StatusNotFound, "", "mapping not found"
|
||||
} else if mapping.UserID != user.ID {
|
||||
return http.StatusForbidden, "", "not allowed to delete mapping"
|
||||
}
|
||||
|
||||
if err := h.languageMappingSrvc.Delete(&models.LanguageMapping{ID: uint(id)}); err != nil {
|
||||
if err := h.languageMappingSrvc.Delete(mapping); err != nil {
|
||||
return http.StatusInternalServerError, "", "could not delete mapping"
|
||||
}
|
||||
|
||||
@ -306,7 +337,7 @@ func (h *SettingsHandler) actionAddLanguageMapping(w http.ResponseWriter, r *htt
|
||||
if h.config.IsDev() {
|
||||
loadTemplates()
|
||||
}
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
extension := r.PostFormValue("extension")
|
||||
language := r.PostFormValue("language")
|
||||
|
||||
@ -332,7 +363,7 @@ func (h *SettingsHandler) actionSetWakatimeApiKey(w http.ResponseWriter, r *http
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
apiKey := r.PostFormValue("api_key")
|
||||
|
||||
// Healthcheck, if a new API key is set, i.e. the feature is activated
|
||||
@ -341,7 +372,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", ""
|
||||
@ -352,7 +383,7 @@ func (h *SettingsHandler) actionImportWaktime(w http.ResponseWriter, r *http.Req
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
if user.WakatimeApiKey == "" {
|
||||
return http.StatusForbidden, "", "not connected to wakatime"
|
||||
}
|
||||
@ -420,13 +451,13 @@ func (h *SettingsHandler) actionRegenerateSummaries(w http.ResponseWriter, r *ht
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
go func(user *models.User) {
|
||||
if err := h.regenerateSummaries(user); err != nil {
|
||||
logbuch.Error("failed to regenerate summaries for user '%s' – %v", user.ID, err)
|
||||
}
|
||||
}(middlewares.GetPrincipal(r))
|
||||
|
||||
if err := h.regenerateSummaries(user); err != nil {
|
||||
return http.StatusInternalServerError, "", "failed to regenerate summaries"
|
||||
}
|
||||
|
||||
return http.StatusOK, "summaries are being regenerated – this may take a few seconds", ""
|
||||
return http.StatusAccepted, "summaries are being regenerated – this may take a up to a couple of minutes, please come back later", ""
|
||||
}
|
||||
|
||||
func (h *SettingsHandler) actionDeleteUser(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
||||
@ -434,7 +465,7 @@ func (h *SettingsHandler) actionDeleteUser(w http.ResponseWriter, r *http.Reques
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
go func(user *models.User) {
|
||||
logbuch.Info("deleting user '%s' shortly", user.ID)
|
||||
time.Sleep(5 * time.Minute)
|
||||
@ -493,7 +524,7 @@ func (h *SettingsHandler) regenerateSummaries(user *models.User) error {
|
||||
}
|
||||
|
||||
func (h *SettingsHandler) buildViewModel(r *http.Request) *view.SettingsViewModel {
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
mappings, _ := h.languageMappingSrvc.GetByUser(user.ID)
|
||||
aliases, _ := h.aliasSrvc.GetByUser(user.ID)
|
||||
aliasMap := make(map[string][]*models.Alias)
|
||||
|
@ -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")
|
||||
@ -52,7 +53,7 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := middlewares.GetPrincipal(r)
|
||||
if user == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
templates[conf.SummaryTemplate].Execute(w, h.buildViewModel(r).WithError("unauthorized"))
|
||||
@ -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)
|
||||
|
@ -18,7 +18,7 @@ func LoadUserSummary(ss services.ISummaryService, r *http.Request) (*models.Summ
|
||||
retrieveSummary = ss.Summarize
|
||||
}
|
||||
|
||||
summary, err := ss.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary)
|
||||
summary, err := ss.Aliased(summaryParams.From, summaryParams.To, summaryParams.User, retrieveSummary, summaryParams.Recompute)
|
||||
if err != nil {
|
||||
return nil, err, http.StatusInternalServerError
|
||||
}
|
||||
|
@ -1,113 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union
|
||||
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
MACHINE = "devmachine"
|
||||
UA = 'wakatime/13.0.7 (Linux-4.15.0-91-generic-x86_64-with-glibc2.4) Python3.8.0.final.0 generator/1.42.1 generator-wakatime/4.0.0'
|
||||
LANGUAGES = {
|
||||
'Go': 'go',
|
||||
'Java': 'java',
|
||||
'JavaScript': 'js',
|
||||
'Python': 'py'
|
||||
}
|
||||
|
||||
|
||||
class Heartbeat:
|
||||
def __init__(
|
||||
self,
|
||||
entity: str,
|
||||
project: str,
|
||||
language: str,
|
||||
time: float,
|
||||
is_write: bool = True,
|
||||
branch: str = 'master',
|
||||
type: str = 'file'
|
||||
):
|
||||
self.entity: str = entity
|
||||
self.project: str = project
|
||||
self.language: str = language
|
||||
self.time: float = time
|
||||
self.is_write: bool = is_write
|
||||
self.branch: str = branch
|
||||
self.type: str = type
|
||||
self.category: Union[str, None] = None
|
||||
|
||||
|
||||
def generate_data(n: int, n_projects: int = 5, n_past_hours: int = 24) -> List[Heartbeat]:
|
||||
data: List[Heartbeat] = []
|
||||
now: datetime = datetime.today()
|
||||
projects: List[str] = [randomword(random.randint(5, 10)) for _ in range(n_projects)]
|
||||
languages: List[str] = list(LANGUAGES.keys())
|
||||
|
||||
for _ in range(n):
|
||||
p: str = random.choice(projects)
|
||||
l: str = random.choice(languages)
|
||||
f: str = randomword(random.randint(2, 8))
|
||||
delta: timedelta = timedelta(
|
||||
hours=random.randint(0, n_past_hours - 1),
|
||||
minutes=random.randint(0, 59),
|
||||
seconds=random.randint(0, 59),
|
||||
milliseconds=random.randint(0, 999),
|
||||
microseconds=random.randint(0, 999)
|
||||
)
|
||||
|
||||
data.append(Heartbeat(
|
||||
entity=f'/home/me/dev/{p}/{f}.{LANGUAGES[l]}',
|
||||
project=p,
|
||||
language=l,
|
||||
time=(now - delta).timestamp()
|
||||
))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def post_data_sync(data: List[Heartbeat], url: str, api_key: str):
|
||||
encoded_key: str = str(base64.b64encode(api_key.encode('utf-8')), 'utf-8')
|
||||
|
||||
for h in tqdm(data):
|
||||
r = requests.post(url, json=[h.__dict__], headers={
|
||||
'User-Agent': UA,
|
||||
'Authorization': f'Basic {encoded_key}',
|
||||
'X-Machine-Name': MACHINE,
|
||||
})
|
||||
if r.status_code != 201:
|
||||
print(r.text)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def randomword(length: int) -> str:
|
||||
letters = string.ascii_lowercase
|
||||
return ''.join(random.choice(letters) for _ in range(length))
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
parser = argparse.ArgumentParser(description='Wakapi test data insertion script.')
|
||||
parser.add_argument('-n', type=int, default=20, help='total number of random heartbeats to generate and insert')
|
||||
parser.add_argument('-u', '--url', type=str, default='http://localhost:3000/api/heartbeat',
|
||||
help='url of your api\'s heartbeats endpoint')
|
||||
parser.add_argument('-k', '--apikey', type=str, required=True,
|
||||
help='your api key (to get one, go to the web interface, create a new user, log in and copy the key)')
|
||||
parser.add_argument('-p', '--projects', type=int, default=5, help='number of different fake projects to generate')
|
||||
parser.add_argument('-o', '--offset', type=int, default=24,
|
||||
help='negative time offset in hours from now for to be used as an interval within which to generate heartbeats for')
|
||||
parser.add_argument('-s', '--seed', type=int, default=2020,
|
||||
help='a seed for initializing the pseudo-random number generator')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parse_arguments()
|
||||
|
||||
random.seed(args.seed)
|
||||
|
||||
data: List[Heartbeat] = generate_data(args.n, args.projects, args.offset)
|
||||
post_data_sync(data, args.url, args.apikey)
|
271
scripts/sample_data.py
Normal file
271
scripts/sample_data.py
Normal file
@ -0,0 +1,271 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Callable
|
||||
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
MACHINE = "devmachine"
|
||||
UA = 'wakatime/13.0.7 (Linux-4.15.0-91-generic-x86_64-with-glibc2.4) Python3.8.0.final.0 generator/1.42.1 generator-wakatime/4.0.0'
|
||||
LANGUAGES = {
|
||||
'Go': 'go',
|
||||
'Java': 'java',
|
||||
'JavaScript': 'js',
|
||||
'Python': 'py'
|
||||
}
|
||||
|
||||
|
||||
class Heartbeat:
|
||||
def __init__(
|
||||
self,
|
||||
entity: str,
|
||||
project: str,
|
||||
language: str,
|
||||
time: float,
|
||||
is_write: bool = True,
|
||||
branch: str = 'master',
|
||||
type: str = 'file'
|
||||
):
|
||||
self.entity: str = entity
|
||||
self.project: str = project
|
||||
self.language: str = language
|
||||
self.time: float = time
|
||||
self.is_write: bool = is_write
|
||||
self.branch: str = branch
|
||||
self.type: str = type
|
||||
self.category: Union[str, None] = None
|
||||
|
||||
|
||||
class ConfigParams:
|
||||
def __init__(self):
|
||||
self.api_url = ''
|
||||
self.api_key = ''
|
||||
self.n = 0
|
||||
self.n_projects = 0
|
||||
self.offset = 0
|
||||
self.seed = 0
|
||||
|
||||
|
||||
def generate_data(n: int, n_projects: int = 5, n_past_hours: int = 24) -> List[Heartbeat]:
|
||||
data: List[Heartbeat] = []
|
||||
now: datetime = datetime.today()
|
||||
projects: List[str] = [randomword(random.randint(5, 10)) for _ in range(n_projects)]
|
||||
languages: List[str] = list(LANGUAGES.keys())
|
||||
|
||||
for _ in range(n):
|
||||
p: str = random.choice(projects)
|
||||
l: str = random.choice(languages)
|
||||
f: str = randomword(random.randint(2, 8))
|
||||
delta: timedelta = timedelta(
|
||||
hours=random.randint(0, n_past_hours - 1),
|
||||
minutes=random.randint(0, 59),
|
||||
seconds=random.randint(0, 59),
|
||||
milliseconds=random.randint(0, 999),
|
||||
microseconds=random.randint(0, 999)
|
||||
)
|
||||
|
||||
data.append(Heartbeat(
|
||||
entity=f'/home/me/dev/{p}/{f}.{LANGUAGES[l]}',
|
||||
project=p,
|
||||
language=l,
|
||||
time=(now - delta).timestamp()
|
||||
))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def post_data_sync(data: List[Heartbeat], url: str, api_key: str):
|
||||
encoded_key: str = str(base64.b64encode(api_key.encode('utf-8')), 'utf-8')
|
||||
|
||||
for h in data:
|
||||
r = requests.post(url, json=[h.__dict__], headers={
|
||||
'User-Agent': UA,
|
||||
'Authorization': f'Basic {encoded_key}',
|
||||
'X-Machine-Name': MACHINE,
|
||||
})
|
||||
if r.status_code != 201:
|
||||
print(r.text)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def make_gui(callback: Callable[[ConfigParams, Callable[[int], None]], None]) -> ('QApplication', 'QWidget'):
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QApplication, QWidget, QFormLayout, QHBoxLayout, QVBoxLayout, QGroupBox, QLabel, \
|
||||
QLineEdit, QSpinBox, QProgressBar, QPushButton
|
||||
|
||||
# Main app
|
||||
app = QApplication([])
|
||||
|
||||
window = QWidget()
|
||||
window.setWindowTitle('Wakapi Sample Data Generator')
|
||||
window.setFixedSize(window.sizeHint())
|
||||
window.setMinimumWidth(350)
|
||||
|
||||
container_layout = QVBoxLayout()
|
||||
|
||||
# Top Controls
|
||||
form_layout_1 = QFormLayout()
|
||||
|
||||
url_input_label = QLabel('URL:')
|
||||
url_input = QLineEdit()
|
||||
url_input.setPlaceholderText('Wakatime API Url')
|
||||
url_input.setText('http://localhost:3000/api')
|
||||
|
||||
api_key_input_label = QLabel('API Key:')
|
||||
api_key_input = QLineEdit()
|
||||
api_key_input.setPlaceholderText(f'{"x"*8}-{"x"*4}-{"x"*4}-{"x"*4}-{"x"*12}')
|
||||
|
||||
form_layout_1.addRow(url_input_label, url_input)
|
||||
form_layout_1.addRow(api_key_input_label, api_key_input)
|
||||
|
||||
# Middle controls
|
||||
form_layout_2 = QFormLayout()
|
||||
params_container = QGroupBox('Parameters')
|
||||
params_container.setLayout(form_layout_2)
|
||||
|
||||
heartbeats_input_label = QLabel('# Heartbeats')
|
||||
heartbeats_input = QSpinBox()
|
||||
heartbeats_input.setMaximum(2147483647)
|
||||
heartbeats_input.setValue(100)
|
||||
|
||||
projects_input_label = QLabel('# Projects:')
|
||||
projects_input = QSpinBox()
|
||||
projects_input.setMinimum(1)
|
||||
projects_input.setValue(5)
|
||||
|
||||
offset_input_label = QLabel('Time Offset (hrs):')
|
||||
offset_input = QSpinBox()
|
||||
offset_input.setMinimum(-2147483647)
|
||||
offset_input.setMaximum(0)
|
||||
offset_input.setValue(-12)
|
||||
|
||||
seed_input_label = QLabel('Random Seed:')
|
||||
seed_input = QSpinBox()
|
||||
seed_input.setMaximum(2147483647)
|
||||
seed_input.setValue(1337)
|
||||
|
||||
form_layout_2.addRow(heartbeats_input_label, heartbeats_input)
|
||||
form_layout_2.addRow(projects_input_label, projects_input)
|
||||
form_layout_2.addRow(offset_input_label, offset_input)
|
||||
form_layout_2.addRow(seed_input_label, seed_input)
|
||||
|
||||
# Bottom controls
|
||||
bottom_layout = QHBoxLayout()
|
||||
|
||||
start_button = QPushButton('Generate')
|
||||
progress_bar = QProgressBar()
|
||||
|
||||
bottom_layout.addWidget(progress_bar)
|
||||
bottom_layout.addWidget(start_button)
|
||||
|
||||
# Wiring up
|
||||
container_layout.addLayout(form_layout_1)
|
||||
container_layout.addWidget(params_container)
|
||||
container_layout.addLayout(bottom_layout)
|
||||
container_layout.setStretch(1, 1)
|
||||
|
||||
window.setLayout(container_layout)
|
||||
|
||||
# Done dialog
|
||||
done_dialog = QWidget()
|
||||
done_dialog.setWindowTitle('Done')
|
||||
done_ok_button = QPushButton('Ok')
|
||||
done_layout = QVBoxLayout()
|
||||
done_layout.addWidget(QLabel('Done!'), alignment=Qt.AlignCenter)
|
||||
done_layout.addWidget(done_ok_button, alignment=Qt.AlignCenter)
|
||||
done_dialog.setFixedSize(done_dialog.sizeHint())
|
||||
done_ok_button.clicked.connect(done_dialog.close)
|
||||
done_dialog.setLayout(done_layout)
|
||||
|
||||
# Logic
|
||||
def parse_params() -> ConfigParams:
|
||||
params = ConfigParams()
|
||||
params.api_url = url_input.text()
|
||||
params.api_key = api_key_input.text()
|
||||
params.n = heartbeats_input.value()
|
||||
params.n_projects = projects_input.value()
|
||||
params.offset = offset_input.value()
|
||||
params.seed = seed_input.value()
|
||||
return params
|
||||
|
||||
def update_progress(inc=1):
|
||||
current = progress_bar.value()
|
||||
updated = current + inc
|
||||
if updated == progress_bar.maximum() - 1:
|
||||
progress_bar.setValue(0)
|
||||
start_button.setEnabled(True)
|
||||
done_dialog.show()
|
||||
return
|
||||
progress_bar.setValue(updated)
|
||||
|
||||
def call_back():
|
||||
params = parse_params()
|
||||
progress_bar.setMaximum(params.n)
|
||||
start_button.setEnabled(False)
|
||||
callback(params, update_progress)
|
||||
|
||||
start_button.clicked.connect(call_back)
|
||||
|
||||
return app, window
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
parser = argparse.ArgumentParser(description='Wakapi test data insertion script.')
|
||||
parser.add_argument('--headless', default=False, help='do not show a gui', action='store_true')
|
||||
parser.add_argument('-n', type=int, default=20, help='total number of random heartbeats to generate and insert')
|
||||
parser.add_argument('-u', '--url', type=str, default='http://localhost:3000/api',
|
||||
help='url of your api\'s heartbeats endpoint')
|
||||
parser.add_argument('-k', '--apikey', type=str,
|
||||
help='your api key (to get one, go to the web interface, create a new user, log in and copy the key)')
|
||||
parser.add_argument('-p', '--projects', type=int, default=5, help='number of different fake projects to generate')
|
||||
parser.add_argument('-o', '--offset', type=int, default=24,
|
||||
help='negative time offset in hours from now for to be used as an interval within which to generate heartbeats for')
|
||||
parser.add_argument('-s', '--seed', type=int, default=2020,
|
||||
help='a seed for initializing the pseudo-random number generator')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def args_to_params(parsed_args: argparse.Namespace) -> (ConfigParams, bool):
|
||||
params = ConfigParams()
|
||||
params.n = parsed_args.n
|
||||
params.n_projects = parsed_args.projects
|
||||
params.offset = parsed_args.offset
|
||||
params.seed = parsed_args.seed
|
||||
params.api_url = parsed_args.url
|
||||
params.api_key = parsed_args.apikey
|
||||
return params, not parsed_args.headless
|
||||
|
||||
|
||||
def randomword(length: int) -> str:
|
||||
letters = string.ascii_lowercase + 'äöü💩' # test utf8 and utf8mb4 characters as well
|
||||
return ''.join(random.choice(letters) for _ in range(length))
|
||||
|
||||
|
||||
def run(params: ConfigParams, update_progress: Callable[[int], None]):
|
||||
random.seed(params.seed)
|
||||
data: List[Heartbeat] = generate_data(
|
||||
params.n,
|
||||
params.n_projects,
|
||||
params.offset * -1 if params.offset < 0 else params.offset
|
||||
)
|
||||
|
||||
for d in data:
|
||||
post_data_sync([d], f'{params.api_url}/heartbeats', params.api_key)
|
||||
update_progress(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
params, show_gui = args_to_params(parse_arguments())
|
||||
if show_gui:
|
||||
app, window = make_gui(callback=run)
|
||||
window.show()
|
||||
app.exec()
|
||||
else:
|
||||
pbar = tqdm(total=params.n)
|
||||
run(params, pbar.update)
|
@ -27,13 +27,32 @@ func (srv *HeartbeatService) Insert(heartbeat *models.Heartbeat) error {
|
||||
}
|
||||
|
||||
func (srv *HeartbeatService) InsertBatch(heartbeats []*models.Heartbeat) error {
|
||||
return srv.repository.InsertBatch(heartbeats)
|
||||
hashes := make(map[string]bool)
|
||||
|
||||
// https://github.com/muety/wakapi/issues/139
|
||||
filteredHeartbeats := make([]*models.Heartbeat, 0, len(heartbeats))
|
||||
for _, hb := range heartbeats {
|
||||
if _, ok := hashes[hb.Hash]; !ok {
|
||||
filteredHeartbeats = append(filteredHeartbeats, hb)
|
||||
hashes[hb.Hash] = true
|
||||
}
|
||||
}
|
||||
|
||||
return srv.repository.InsertBatch(filteredHeartbeats)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ func (srv *MiscService) runCountTotalTime() error {
|
||||
|
||||
func (srv *MiscService) countTotalTimeWorker(jobs <-chan *CountTotalTimeJob, results chan<- *CountTotalTimeResult) {
|
||||
for job := range jobs {
|
||||
if result, err := srv.summaryService.Aliased(time.Time{}, time.Now(), &models.User{ID: job.UserID}, srv.summaryService.Retrieve); err != nil {
|
||||
if result, err := srv.summaryService.Aliased(time.Time{}, time.Now(), &models.User{ID: job.UserID}, srv.summaryService.Retrieve, false); err != nil {
|
||||
logbuch.Error("failed to count total for user %s: %v", job.UserID, err)
|
||||
} else {
|
||||
logbuch.Info("successfully counted total for user %s", job.UserID)
|
||||
|
@ -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)
|
||||
@ -51,7 +53,7 @@ type ILanguageMappingService interface {
|
||||
}
|
||||
|
||||
type ISummaryService interface {
|
||||
Aliased(time.Time, time.Time, *models.User, SummaryRetriever) (*models.Summary, error)
|
||||
Aliased(time.Time, time.Time, *models.User, SummaryRetriever, bool) (*models.Summary, error)
|
||||
Retrieve(time.Time, time.Time, *models.User) (*models.Summary, error)
|
||||
Summarize(time.Time, time.Time, *models.User) (*models.Summary, error)
|
||||
GetLatestByUser() ([]*models.TimeByUser, 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)
|
||||
|
@ -36,10 +36,10 @@ func NewSummaryService(summaryRepo repositories.ISummaryRepository, heartbeatSer
|
||||
|
||||
// Public summary generation methods
|
||||
|
||||
func (srv *SummaryService) Aliased(from, to time.Time, user *models.User, f SummaryRetriever) (*models.Summary, error) {
|
||||
func (srv *SummaryService) Aliased(from, to time.Time, user *models.User, f SummaryRetriever, skipCache bool) (*models.Summary, error) {
|
||||
// Check cache
|
||||
cacheKey := srv.getHash(from.String(), to.String(), user.ID, "--aliased")
|
||||
if cacheResult, ok := srv.cache.Get(cacheKey); ok {
|
||||
if cacheResult, ok := srv.cache.Get(cacheKey); ok && !skipCache {
|
||||
return cacheResult.(*models.Summary), nil
|
||||
}
|
||||
|
||||
|
@ -253,7 +253,7 @@ func (suite *SummaryServiceTestSuite) TestSummaryService_Aliased() {
|
||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, models.SummaryProject, TestProject1).Return(TestProject2, nil)
|
||||
suite.AliasService.On("GetAliasOrDefault", TestUserId, mock.Anything, mock.Anything).Return("", nil)
|
||||
|
||||
result, err = sut.Aliased(from, to, suite.TestUser, sut.Summarize)
|
||||
result, err = sut.Aliased(from, to, suite.TestUser, sut.Summarize, false)
|
||||
|
||||
assert.Nil(suite.T(), err)
|
||||
assert.NotNil(suite.T(), result)
|
||||
|
@ -56,11 +56,22 @@ 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,
|
||||
Email: signup.Email,
|
||||
ApiKey: uuid.NewV4().String(),
|
||||
Password: signup.Password,
|
||||
IsAdmin: isAdmin,
|
||||
}
|
||||
|
||||
if hash, err := utils.HashBcrypt(u.Password, srv.Config.Security.PasswordSalt); err != nil {
|
||||
|
@ -1,3 +1,3 @@
|
||||
sonar.exclusions=**/*_test.go,.idea/**,.vscode/**,mocks/**
|
||||
sonar.exclusions=**/*_test.go,.idea/**,.vscode/**,mocks/**,static/**
|
||||
sonar.tests=.
|
||||
sonar.go.coverage.reportPaths=coverage/coverage.out
|
@ -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 {
|
||||
@ -105,7 +112,7 @@ function draw(subselection) {
|
||||
xAxes: [{
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: 'Minutes'
|
||||
labelString: 'Seconds'
|
||||
}
|
||||
}]
|
||||
},
|
||||
@ -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 |
105251
static/assets/vendor/tailwind.css
vendored
Normal file
105251
static/assets/vendor/tailwind.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1267
static/assets/vendor/tailwind.dist.css
vendored
Normal file
1267
static/assets/vendor/tailwind.dist.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
static/assets/vendor/tailwind.min.css
vendored
1
static/assets/vendor/tailwind.min.css
vendored
File diff suppressed because one or more lines are too long
7
tailwind.config.js
Normal file
7
tailwind.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
purge: {
|
||||
enabled: true,
|
||||
mode: 'all',
|
||||
content: ['./views/*.tpl.html']
|
||||
}
|
||||
}
|
@ -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 {
|
||||
@ -66,7 +71,7 @@ func ResolveInterval(interval *models.IntervalKey) (err error, from, to time.Tim
|
||||
}
|
||||
|
||||
func ParseSummaryParams(r *http.Request) (*models.SummaryParams, error) {
|
||||
user := r.Context().Value(models.UserKey).(*models.User)
|
||||
user := extractUser(r)
|
||||
params := r.URL.Query()
|
||||
|
||||
var err error
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,3 +108,13 @@ func ParseSummaryParams(r *http.Request) (*models.SummaryParams, error) {
|
||||
Recompute: recompute,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func extractUser(r *http.Request) *models.User {
|
||||
type principalGetter interface {
|
||||
GetPrincipal() *models.User
|
||||
}
|
||||
if p := r.Context().Value("principal"); p != nil {
|
||||
return p.(principalGetter).GetPrincipal()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
1.23.3
|
||||
1.25.4
|
||||
|
@ -1,2 +1,2 @@
|
||||
<script src="assets/vendor/seedrandom.min.js" integrity="sha384-bFS5CG904xYIgxBcrDF4KFNXuM7KeSGsSvS/QTaDqMTEdbaaxjg2Y2TSU3Ygs7wG" crossorigin="anonymous"></script>
|
||||
<script src="assets/vendor/Chart.bundle.min.js" integrity="sha384-mZ3q69BYmd4GxHp59G3RrsaFdWDxVSoqd7oVYuWRm2qiXrduT63lQtlhdD9lKbm3" crossorigin="anonymous"></script>
|
||||
<script src="assets/vendor/seedrandom.min.js"></script>
|
||||
<script src="assets/vendor/Chart.bundle.min.js"></script>
|
@ -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>
|
@ -7,6 +7,10 @@
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon-16x16.png">
|
||||
<link rel="manifest" href="assets/site.webmanifest">
|
||||
<link href="assets/vendor/roboto.css" rel="stylesheet">
|
||||
<link href="assets/vendor/tailwind.min.css" rel="stylesheet">
|
||||
{{ if isDev }}
|
||||
<link href="assets/vendor/tailwind.css" rel="stylesheet">
|
||||
{{ else }}
|
||||
<link href="assets/vendor/tailwind.dist.css" rel="stylesheet">
|
||||
{{ end }}
|
||||
<link href="assets/app.css" rel="stylesheet">
|
||||
</head>
|
@ -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,9 +32,35 @@
|
||||
{{ 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">
|
||||
<h2 class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block"
|
||||
id="email-heading">
|
||||
Change E-Mail Address
|
||||
</h2>
|
||||
</summary>
|
||||
<div class="w-full">
|
||||
<form class="mt-10" action="" method="post">
|
||||
<input type="hidden" name="action" value="update_user">
|
||||
<div class="mb-8 flex justify-between items-center space-x-4">
|
||||
<label class="inline-block text-sm text-gray-500" for="password_old">E-Mail Address</label>
|
||||
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded flex-grow py-1 px-3"
|
||||
type="email" id="email"
|
||||
name="email" placeholder="Enter your e-mail address"
|
||||
value="{{ .User.Email }}">
|
||||
<button type="submit"
|
||||
class="py-1 px-3 rounded bg-green-700 hover:bg-green-800 text-white text-sm">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-gray-300 text-sm">E-Mail address is optional, but required for some features that you cannot use else. Also, if you do not add an e-mail address, you will not be able to reset your password in case you forget it.</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="mb-8 pb-8 border-b border-gray-700">
|
||||
<summary class="cursor-pointer">
|
||||
<h2 class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block" id="password">
|
||||
Change Password
|
||||
@ -534,26 +560,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', () => {
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
{{ template "head.tpl.html" . }}
|
||||
|
||||
<body class="bg-gray-800 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
||||
<body class="bg-gray-850 text-gray-700 p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
||||
<div class="w-full flex justify-center">
|
||||
<div class="flex items-center justify-between max-w-4xl flex-grow">
|
||||
<div><a href="" class="text-gray-500 text-sm">← Go back</a></div>
|
||||
@ -37,6 +37,13 @@
|
||||
type="text" id="username"
|
||||
name="username" placeholder="Choose a username" minlength="1" required autofocus>
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<label class="inline-block text-sm mb-1 text-gray-500" for="email">E-Mail</label>
|
||||
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
||||
type="email" id="email"
|
||||
name="email" placeholder="Optionally add your e-mail address">
|
||||
<div class="text-xs text-gray-500 mt-2 italic">E-Mail address is optional, but required for some features that you cannot use else. Also, if you do not add an e-mail address, you will not be able to reset your password in case you forget it.</div>
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<label class="inline-block text-sm mb-1 text-gray-500" for="password">Password</label>
|
||||
<input class="shadow appearance-none bg-gray-800 focus:bg-gray-700 text-gray-300 border-green-700 focus:border-gray-500 border rounded w-full py-1 px-3"
|
||||
@ -49,6 +56,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,35 +36,59 @@
|
||||
</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" max="{{ .ToTime.T | simpledate }}" 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" min="{{ .FromTime.T | simpledate }}" 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="w-full lg:w-1/2 p-1" style="max-width: 100vw;">
|
||||
<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">
|
||||
<div class="flex justify-between">
|
||||
<div class="w-1/4 flex-1"></div>
|
||||
@ -76,12 +100,11 @@
|
||||
</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>
|
||||
<div class="w-full lg:w-1/2 p-1">
|
||||
<div class="w-full lg:w-1/2 p-1" style="max-width: 100vw;">
|
||||
<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="os-container" style="height: 300px">
|
||||
<div class="flex justify-between">
|
||||
<div class="w-1/4 flex-1"></div>
|
||||
@ -93,12 +116,11 @@
|
||||
</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>
|
||||
<div class="w-full lg:w-1/2 p-1">
|
||||
<div class="w-full lg:w-1/2 p-1" style="max-width: 100vw;">
|
||||
<div class="p-4 pb-10 bg-gray-900 border border-gray-700 text-gray-300 rounded-md shadow m-2 flex flex-col relative" id="language-container" style="height: 300px">
|
||||
<div class="flex justify-between">
|
||||
<div class="w-1/4 flex-1"></div>
|
||||
@ -110,12 +132,11 @@
|
||||
</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>
|
||||
<div class="w-full lg:w-1/2 p-1">
|
||||
<div class="w-full lg:w-1/2 p-1" style="max-width: 100vw;">
|
||||
<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="editor-container" style="height: 300px">
|
||||
<div class="flex justify-between">
|
||||
<div class="w-1/4 flex-1"></div>
|
||||
@ -127,12 +148,11 @@
|
||||
</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>
|
||||
<div class="w-full lg:w-1/2 p-1">
|
||||
<div class="w-full lg:w-1/2 p-1" style="max-width: 100vw;">
|
||||
<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="machine-container" style="height: 300px">
|
||||
<div class="flex justify-between">
|
||||
<div class="w-1/4 flex-1"></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 }}
|
||||
@ -167,6 +224,17 @@
|
||||
wakapiData.editors = {{ .Editors | json }}
|
||||
wakapiData.languages = {{ .Languages | json }}
|
||||
wakapiData.machines = {{ .Machines | json }}
|
||||
|
||||
document.getElementById("to-date-picker").onchange = function () {
|
||||
var input = document.getElementById("from-date-picker");
|
||||
input.setAttribute("max", this.value);
|
||||
}
|
||||
|
||||
document.getElementById("from-date-picker").onchange = function () {
|
||||
var input = document.getElementById("to-date-picker");
|
||||
input.setAttribute("min", this.value);
|
||||
}
|
||||
|
||||
</script>
|
||||
<script src="assets/app.js"></script>
|
||||
|
||||
|
Reference in New Issue
Block a user