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_PASSWORD=mysecretpassword
WAKAPI_DB_HOST=localhost

View File

@ -1,7 +1,7 @@
# 📈 wakapi
**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)
@ -61,9 +61,6 @@ However, if you want to expose your wakapi instance to the public anyway, you ne
## Todo
* User sign up and log in
* Additional endpoints for retrieving statistics data
* Enhanced UI
* Loading spinner
* Responsiveness
* Support for SQLite database
* Unit tests

View File

@ -31,6 +31,7 @@ func readConfig() *models.Config {
log.Fatal(err)
}
env, _ := os.LookupEnv("ENV")
dbUser, valid := os.LookupEnv("WAKAPI_DB_USER")
dbPassword, valid := os.LookupEnv("WAKAPI_DB_PASSWORD")
dbHost, valid := os.LookupEnv("WAKAPI_DB_HOST")
@ -62,6 +63,7 @@ func readConfig() *models.Config {
}
return &models.Config{
Env: env,
Port: port,
Addr: addr,
DbHost: dbHost,

View File

@ -1,6 +1,7 @@
package models
type Config struct {
Env string
Port int
Addr string
DbHost string
@ -12,3 +13,7 @@ type Config struct {
DbMaxConn uint
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() {
m.loadTemplates()
m.Initialized = true
}
func (m *SummaryHandler) loadTemplates() {
indexTplPath := "views/index.tpl.html"
indexTpl, err := template.New(path.Base(indexTplPath)).Funcs(template.FuncMap{
"json": utils.Json,
"date": utils.FormatDateHuman,
}).ParseFiles(indexTplPath)
if err != nil {
panic(err)
}
m.indexTemplate = indexTpl
m.Initialized = true
}
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()
}
if h.SummarySrvc.Config.IsDev() {
h.loadTemplates()
}
summary, err, status := loadUserSummary(r, h.SummarySrvc)
if err != nil {
w.WriteHeader(status)

View File

@ -1,44 +1,3 @@
body {
font-family: '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;
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
}

View File

@ -18,6 +18,10 @@ func FormatDate(date time.Time) string {
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) {
re := regexp.MustCompile(`^wakatime\/[\d+.]+\s\((\w+).*\)\s.+\s(\w+)\/.+$`)
groups := re.FindAllStringSubmatch(ua, -1)

View File

@ -1,54 +1,74 @@
<html>
<head>
<title>Coding Stats</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>Wakapi Coding Statistics</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
<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>
<body>
<h1>Wakapi</h1>
<h3>Your Coding Statistics Dashboard</h3>
<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 class="grid-container" id="grid-container">
<div class="header-container">
<p>
<strong>Total:</strong> <span id="total-span"></span><br>
</p>
<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">
<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>
</div>
<div class="projects-container" id="projects-container">
<canvas id="chart-projects"></canvas>
<div class="text-white text-sm flex items-center justify-center mt-4">
<a href="/?interval=today" class="m-1 border-b border-green-700">Today (live)</a>
<a href="/?interval=day" class="m-1 border-b border-green-700">Yesterday</a>
<a href="/?interval=week" class="m-1 border-b border-green-700">This Week</a>
<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 class="os-container" id="os-container">
<canvas id="chart-os"></canvas>
</div>
<div class="editor-container" id="editor-container">
<canvas id="chart-editor"></canvas>
</div>
<div class="language-container" id="language-container">
<canvas id="chart-language"></canvas>
</div>
</div>
<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>
</div>
</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>
</div>
</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>
</div>
</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>
</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/Chart.js/2.7.3/Chart.bundle.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>
let wakapiData = {}
wakapiData.projects = {{ json .Projects }}
wakapiData.operatingSystems = {{ .OperatingSystems | json }}
wakapiData.editors = {{ .Editors | json }}
wakapiData.languages = {{ .Languages | json }}
</script>
<script src="/assets/app.js"></script>
<script>
let wakapiData = {}
wakapiData.projects = {{ json .Projects }}
wakapiData.operatingSystems = {{ .OperatingSystems | json }}
wakapiData.editors = {{ .Editors | json }}
wakapiData.languages = {{ .Languages | json }}
</script>
<script src="/assets/app.js"></script>
</body>
</html>