1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00
This commit is contained in:
Ferdinand Mütsch 2020-02-20 15:39:56 +01:00
parent b7f700e7a5
commit 6d3891b398
8 changed files with 85 additions and 91 deletions

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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"
}

View File

@ -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)

View File

@ -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;
} }

View File

@ -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)

View File

@ -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>