mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Resolve #11.
This commit is contained in:
parent
b7f700e7a5
commit
6d3891b398
@ -1,3 +1,4 @@
|
|||||||
|
ENV=prod
|
||||||
WAKAPI_DB_USER=myuser
|
WAKAPI_DB_USER=myuser
|
||||||
WAKAPI_DB_PASSWORD=mysecretpassword
|
WAKAPI_DB_PASSWORD=mysecretpassword
|
||||||
WAKAPI_DB_HOST=localhost
|
WAKAPI_DB_HOST=localhost
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# 📈 wakapi
|
# 📈 wakapi
|
||||||
**A minimalist, self-hosted WakaTime-compatible backend for coding statistics**
|
**A minimalist, self-hosted WakaTime-compatible backend for coding statistics**
|
||||||
|
|
||||||
![Wakapi screenshot](https://anchr.io/i/zCVbN.png)
|
![Wakapi screenshot](https://anchr.io/i/bxQ69.png)
|
||||||
|
|
||||||
[![Buy me a coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoff.ee/n1try)
|
[![Buy me a coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoff.ee/n1try)
|
||||||
|
|
||||||
@ -61,9 +61,6 @@ However, if you want to expose your wakapi instance to the public anyway, you ne
|
|||||||
## Todo
|
## Todo
|
||||||
* User sign up and log in
|
* User sign up and log in
|
||||||
* Additional endpoints for retrieving statistics data
|
* Additional endpoints for retrieving statistics data
|
||||||
* Enhanced UI
|
|
||||||
* Loading spinner
|
|
||||||
* Responsiveness
|
|
||||||
* Support for SQLite database
|
* Support for SQLite database
|
||||||
* Unit tests
|
* Unit tests
|
||||||
|
|
||||||
|
2
main.go
2
main.go
@ -31,6 +31,7 @@ func readConfig() *models.Config {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
env, _ := os.LookupEnv("ENV")
|
||||||
dbUser, valid := os.LookupEnv("WAKAPI_DB_USER")
|
dbUser, valid := os.LookupEnv("WAKAPI_DB_USER")
|
||||||
dbPassword, valid := os.LookupEnv("WAKAPI_DB_PASSWORD")
|
dbPassword, valid := os.LookupEnv("WAKAPI_DB_PASSWORD")
|
||||||
dbHost, valid := os.LookupEnv("WAKAPI_DB_HOST")
|
dbHost, valid := os.LookupEnv("WAKAPI_DB_HOST")
|
||||||
@ -62,6 +63,7 @@ func readConfig() *models.Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &models.Config{
|
return &models.Config{
|
||||||
|
Env: env,
|
||||||
Port: port,
|
Port: port,
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
DbHost: dbHost,
|
DbHost: dbHost,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
Env string
|
||||||
Port int
|
Port int
|
||||||
Addr string
|
Addr string
|
||||||
DbHost string
|
DbHost string
|
||||||
@ -12,3 +13,7 @@ type Config struct {
|
|||||||
DbMaxConn uint
|
DbMaxConn uint
|
||||||
CustomLanguages map[string]string
|
CustomLanguages map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) IsDev() bool {
|
||||||
|
return c.Env == "dev"
|
||||||
|
}
|
||||||
|
@ -28,18 +28,20 @@ type SummaryHandler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *SummaryHandler) Init() {
|
func (m *SummaryHandler) Init() {
|
||||||
|
m.loadTemplates()
|
||||||
|
m.Initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SummaryHandler) loadTemplates() {
|
||||||
indexTplPath := "views/index.tpl.html"
|
indexTplPath := "views/index.tpl.html"
|
||||||
indexTpl, err := template.New(path.Base(indexTplPath)).Funcs(template.FuncMap{
|
indexTpl, err := template.New(path.Base(indexTplPath)).Funcs(template.FuncMap{
|
||||||
"json": utils.Json,
|
"json": utils.Json,
|
||||||
|
"date": utils.FormatDateHuman,
|
||||||
}).ParseFiles(indexTplPath)
|
}).ParseFiles(indexTplPath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.indexTemplate = indexTpl
|
m.indexTemplate = indexTpl
|
||||||
|
|
||||||
m.Initialized = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SummaryHandler) Get(w http.ResponseWriter, r *http.Request) {
|
func (h *SummaryHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -72,6 +74,10 @@ func (h *SummaryHandler) Index(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.Init()
|
h.Init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if h.SummarySrvc.Config.IsDev() {
|
||||||
|
h.loadTemplates()
|
||||||
|
}
|
||||||
|
|
||||||
summary, err, status := loadUserSummary(r, h.SummarySrvc)
|
summary, err, status := loadUserSummary(r, h.SummarySrvc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
|
@ -1,44 +1,3 @@
|
|||||||
body {
|
body {
|
||||||
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 10px 0 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-container {
|
|
||||||
width: 75%;
|
|
||||||
display: grid;
|
|
||||||
grid-template-areas: 'header header header' 'sec1 sec1 sec2' 'sec1 sec1 sec3' 'sec1 sec1 sec4' 'footer footer footer';
|
|
||||||
grid-gap: 10px;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-container {
|
|
||||||
grid-area: sec1
|
|
||||||
}
|
|
||||||
|
|
||||||
.os-container {
|
|
||||||
grid-area: sec2
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-container {
|
|
||||||
grid-area: sec3
|
|
||||||
}
|
|
||||||
|
|
||||||
.language-container {
|
|
||||||
grid-area: sec4
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-container {
|
|
||||||
grid-area: header
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
width: 300px;
|
|
||||||
}
|
}
|
@ -18,6 +18,10 @@ func FormatDate(date time.Time) string {
|
|||||||
return date.Format("2006-01-02 15:04:05")
|
return date.Format("2006-01-02 15:04:05")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FormatDateHuman(date time.Time) string {
|
||||||
|
return date.Format("Mon, 02 Jan 2006 15:04")
|
||||||
|
}
|
||||||
|
|
||||||
func ParseUserAgent(ua string) (string, string, error) {
|
func ParseUserAgent(ua string) (string, string, error) {
|
||||||
re := regexp.MustCompile(`^wakatime\/[\d+.]+\s\((\w+).*\)\s.+\s(\w+)\/.+$`)
|
re := regexp.MustCompile(`^wakatime\/[\d+.]+\s\((\w+).*\)\s.+\s(\w+)\/.+$`)
|
||||||
groups := re.FindAllStringSubmatch(ua, -1)
|
groups := re.FindAllStringSubmatch(ua, -1)
|
||||||
|
@ -1,42 +1,62 @@
|
|||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>Coding Stats</title>
|
<title>Wakapi – Coding Statistics</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
|
||||||
<link rel="icon" data-emoji="📊" type="image/png">
|
<link rel="icon" data-emoji="📊" type="image/png">
|
||||||
<link rel="stylesheet" href="/assets/app.css">
|
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
|
||||||
|
<link href="/assets/app.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body class="bg-gray-800 text-gray-700 p-4 pt-12 flex flex-col min-h-screen max-w-screen-xl mx-auto justify-center">
|
||||||
<h1>Wakapi</h1>
|
<div class="flex items-center justify-center">
|
||||||
<h3>Your Coding Statistics Dashboard</h3>
|
<h1 class="font-semibold text-2xl text-white m-0 border-b-4 border-green-700">Your Coding Statistics 🤓</h1>
|
||||||
<div>
|
|
||||||
<a href="/?interval=today">Today (live)</a>
|
|
||||||
<a href="/?interval=day">Yesterday</a>
|
|
||||||
<a href="/?interval=week">This Week</a>
|
|
||||||
<a href="/?interval=month">This Month</a>
|
|
||||||
<a href="/?interval=year">This Year</a>
|
|
||||||
<a href="/?interval=any">All Time</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-container" id="grid-container">
|
<div class="text-white text-sm flex items-center justify-center mt-4">
|
||||||
<div class="header-container">
|
<a href="/?interval=today" class="m-1 border-b border-green-700">Today (live)</a>
|
||||||
<p>
|
<a href="/?interval=day" class="m-1 border-b border-green-700">Yesterday</a>
|
||||||
<strong>Total:</strong> <span id="total-span"></span><br>
|
<a href="/?interval=week" class="m-1 border-b border-green-700">This Week</a>
|
||||||
</p>
|
<a href="/?interval=month" class="m-1 border-b border-green-700">This Month</a>
|
||||||
|
<a href="/?interval=year" class="m-1 border-b border-green-700">This Year</a>
|
||||||
|
<a href="/?interval=any" class="m-1 border-b border-green-700">All Time</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="projects-container" id="projects-container">
|
<main class="mt-12 flex-grow" id="grid-container">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="p-1">
|
||||||
|
<div class="flex justify-center p-4 bg-white rounded shadow">
|
||||||
|
<p class="mx-2"><strong>▶️</strong> <span title="Start Time">{{ .FromTime | date }}</span></p>
|
||||||
|
<p class="mx-2"><strong>⏹</strong> <span title="End Time">{{ .ToTime | date }}</span></p>
|
||||||
|
<p class="mx-2"><strong>⏱</strong> <span id="total-span" title="Total Hours"></span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap justify-center">
|
||||||
|
<div class="w-full lg:w-1/2 p-1">
|
||||||
|
<div class="p-4 bg-white rounded shadow m-2" id="projects-container">
|
||||||
<canvas id="chart-projects"></canvas>
|
<canvas id="chart-projects"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="os-container" id="os-container">
|
</div>
|
||||||
|
<div class="w-full lg:w-1/2 p-1">
|
||||||
|
<div class="p-4 bg-white rounded shadow m-2" id="os-container">
|
||||||
<canvas id="chart-os"></canvas>
|
<canvas id="chart-os"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-container" id="editor-container">
|
</div>
|
||||||
|
<div class="w-full lg:w-1/2 p-1">
|
||||||
|
<div class="p-4 bg-white rounded shadow m-2" id="editor-container">
|
||||||
<canvas id="chart-editor"></canvas>
|
<canvas id="chart-editor"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="language-container" id="language-container">
|
</div>
|
||||||
|
<div class="w-full lg:w-1/2 p-1">
|
||||||
|
<div class="p-4 bg-white rounded shadow m-2" id="language-container">
|
||||||
<canvas id="chart-language"></canvas>
|
<canvas id="chart-language"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="w-full text-center text-gray-300 text-xs mt-12">
|
||||||
|
Made by <a href="https://muetsch.io" class="border-b border-green-700">Ferdinand Mütsch</a> as <a href="https://github.com/n1try/wakapi" class="border-b border-green-700">open-source</a>.
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.4/seedrandom.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.4/seedrandom.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.bundle.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.bundle.min.js"></script>
|
||||||
|
Loading…
Reference in New Issue
Block a user