mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
81835a3d88 | |||
30de96950b | |||
11291b0d6c | |||
f0ac0f6153 | |||
6aad1633e1 | |||
c07a4d71a0 | |||
dff0b742fc | |||
4f65f94766 | |||
825663acde | |||
f399fd4ea7 | |||
87fadf46f7 |
19
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Bug
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is. Please briefly describe how to reproduce the bug as well as _expected_ vs. _actual_ behavior. Optionally include screenshots and server logs, if helpful.
|
||||
|
||||
**System information**
|
||||
Please provide information on:
|
||||
* Wakapi version
|
||||
* Operating system
|
||||
* If Linux: which distro?
|
||||
* If Docker: which image and tag?
|
||||
* Database (SQLite, MySQL, ... ?)
|
10
.github/ISSUE_TEMPLATE/other.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/other.md
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Other (feature request, question, ...)
|
||||
about: Anything else
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
@ -31,7 +31,7 @@ RUN cp /src/wakapi . && \
|
||||
FROM alpine:3
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk update && apk add bash ca-certificates && rm -rf /var/cache/apk
|
||||
RUN apk update && apk add bash ca-certificates tzdata && rm -rf /var/cache/apk
|
||||
|
||||
# See README.md and config.default.yml for all config options
|
||||
ENV ENVIRONMENT prod
|
||||
|
@ -412,7 +412,11 @@ Wakapi adds a "padding" of two minutes before the third heartbeat. This is why t
|
||||
</details>
|
||||
|
||||
## 🙏 Thanks
|
||||
I highly appreciate the efforts of [@alanhamlett](https://github.com/alanhamlett) and the WakaTime team and am thankful for their software being open source.
|
||||
I highly appreciate the efforts of **[@alanhamlett](https://github.com/alanhamlett)** and the WakaTime team and am thankful for their software being open source.
|
||||
|
||||
Moreover, thanks to **[JetBrains](https://jb.gg/OpenSource)** for supporting this project as part of their open-source program.
|
||||
|
||||

|
||||
|
||||
## 📓 License
|
||||
GPL-v3 @ [Ferdinand Mütsch](https://muetsch.io)
|
||||
|
@ -22,6 +22,7 @@ type Heartbeat struct {
|
||||
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
|
||||
UserAgent string `json:"user_agent" hash:"ignore"`
|
||||
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"`
|
||||
|
@ -160,10 +160,6 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.RowsAffected != 1 {
|
||||
return nil, errors.New("nothing updated")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,8 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
opSys, editor, _ := utils.ParseUserAgent(r.Header.Get("User-Agent"))
|
||||
userAgent := r.Header.Get("User-Agent")
|
||||
opSys, editor, _ := utils.ParseUserAgent(userAgent)
|
||||
machineName := r.Header.Get("X-Machine-Name")
|
||||
|
||||
for _, hb := range heartbeats {
|
||||
@ -88,6 +89,7 @@ func (h *HeartbeatApiHandler) Post(w http.ResponseWriter, r *http.Request) {
|
||||
hb.Machine = machineName
|
||||
hb.User = user
|
||||
hb.UserID = user.ID
|
||||
hb.UserAgent = userAgent
|
||||
|
||||
if !hb.Valid() {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
@ -2,27 +2,24 @@ package routes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/muety/wakapi/views"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"github.com/muety/wakapi/views"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
type action func(w http.ResponseWriter, r *http.Request) (int, string, string)
|
||||
|
||||
var templates map[string]*template.Template
|
||||
|
||||
func Init() {
|
||||
loadTemplates()
|
||||
}
|
||||
|
||||
func DefaultTemplateFuncs() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"json": utils.Json,
|
||||
@ -58,44 +55,6 @@ func DefaultTemplateFuncs() template.FuncMap {
|
||||
}
|
||||
}
|
||||
|
||||
func loadTemplates() {
|
||||
tpls := template.New("").Funcs(DefaultTemplateFuncs())
|
||||
templates = make(map[string]*template.Template)
|
||||
|
||||
// Use local file system when in 'dev' environment, go embed file system otherwise
|
||||
templateFs := config.ChooseFS("views", views.TemplateFiles)
|
||||
|
||||
files, err := fs.ReadDir(templateFs, ".")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
tplName := file.Name()
|
||||
if file.IsDir() || path.Ext(tplName) != ".html" {
|
||||
continue
|
||||
}
|
||||
|
||||
templateFile, err := templateFs.Open(tplName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
templateData, err := ioutil.ReadAll(templateFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
templateFile.Close()
|
||||
|
||||
tpl, err := tpls.New(tplName).Parse(string(templateData))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
templates[tplName] = tpl
|
||||
}
|
||||
}
|
||||
|
||||
func typeName(t uint8) string {
|
||||
if t == models.SummaryProject {
|
||||
return "project"
|
||||
@ -118,6 +77,16 @@ func typeName(t uint8) string {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func loadTemplates() {
|
||||
// Use local file system when in 'dev' environment, go embed file system otherwise
|
||||
templateFs := config.ChooseFS("views", views.TemplateFiles)
|
||||
if tpls, err := utils.LoadTemplates(templateFs, DefaultTemplateFuncs()); err == nil {
|
||||
templates = tpls
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultErrorRedirectTarget() string {
|
||||
return fmt.Sprintf("%s/?error=unauthorized", config.Get().Server.BasePath)
|
||||
}
|
||||
|
@ -106,6 +106,7 @@ func (w *WakatimeHeartbeatImporter) ImportAll(user *models.User) <-chan *models.
|
||||
}
|
||||
|
||||
// https://wakatime.com/api/v1/users/current/heartbeats?date=2021-02-05
|
||||
// https://pastr.de/p/b5p4od5s8w0pfntmwoi117jy
|
||||
func (w *WakatimeHeartbeatImporter) fetchHeartbeats(day string) ([]*wakatime.HeartbeatEntry, error) {
|
||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
@ -134,6 +135,7 @@ func (w *WakatimeHeartbeatImporter) fetchHeartbeats(day string) ([]*wakatime.Hea
|
||||
}
|
||||
|
||||
// https://wakatime.com/api/v1/users/current/all_time_since_today
|
||||
// https://pastr.de/p/w8xb4biv575pu32pox7jj2gr
|
||||
func (w *WakatimeHeartbeatImporter) fetchRange() (time.Time, time.Time, error) {
|
||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
@ -168,6 +170,7 @@ func (w *WakatimeHeartbeatImporter) fetchRange() (time.Time, time.Time, error) {
|
||||
}
|
||||
|
||||
// https://wakatime.com/api/v1/users/current/user_agents
|
||||
// https://pastr.de/p/05k5do8q108k94lic4lfl3pc
|
||||
func (w *WakatimeHeartbeatImporter) fetchUserAgents() (map[string]*wakatime.UserAgentEntry, error) {
|
||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
@ -195,6 +198,7 @@ func (w *WakatimeHeartbeatImporter) fetchUserAgents() (map[string]*wakatime.User
|
||||
}
|
||||
|
||||
// https://wakatime.com/api/v1/users/current/machine_names
|
||||
// https://pastr.de/p/v58cv0xrupp3zvyyv8o6973j
|
||||
func (w *WakatimeHeartbeatImporter) fetchMachineNames() (map[string]*wakatime.MachineEntry, error) {
|
||||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
@ -261,6 +265,7 @@ func mapHeartbeat(
|
||||
Editor: ua.Editor,
|
||||
OperatingSystem: ua.Os,
|
||||
Machine: ma.Value,
|
||||
UserAgent: ua.Value,
|
||||
Time: entry.Time,
|
||||
Origin: OriginWakatime,
|
||||
OriginId: entry.Id,
|
||||
|
@ -7,12 +7,10 @@ import (
|
||||
"github.com/muety/wakapi/routes"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/utils"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"github.com/muety/wakapi/views/mail"
|
||||
"time"
|
||||
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/views"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -33,6 +31,7 @@ type SendingService interface {
|
||||
type MailService struct {
|
||||
config *conf.Config
|
||||
sendingService SendingService
|
||||
templates utils.TemplateMap
|
||||
}
|
||||
|
||||
func NewMailService() services.IMailService {
|
||||
@ -49,11 +48,18 @@ func NewMailService() services.IMailService {
|
||||
}
|
||||
}
|
||||
|
||||
return &MailService{sendingService: sendingService, config: config}
|
||||
// Use local file system when in 'dev' environment, go embed file system otherwise
|
||||
templateFs := conf.ChooseFS("views/mail", mail.TemplateFiles)
|
||||
templates, err := utils.LoadTemplates(templateFs, routes.DefaultTemplateFuncs())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &MailService{sendingService: sendingService, config: config, templates: templates}
|
||||
}
|
||||
|
||||
func (m *MailService) SendPasswordReset(recipient *models.User, resetLink string) error {
|
||||
tpl, err := getPasswordResetTemplate(PasswordResetTplData{ResetLink: resetLink})
|
||||
tpl, err := m.getPasswordResetTemplate(PasswordResetTplData{ResetLink: resetLink})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -67,7 +73,7 @@ func (m *MailService) SendPasswordReset(recipient *models.User, resetLink string
|
||||
}
|
||||
|
||||
func (m *MailService) SendWakatimeFailureNotification(recipient *models.User, numFailures int) error {
|
||||
tpl, err := getWakatimeFailureNotificationTemplate(WakatimeFailureNotificationNotificationTplData{
|
||||
tpl, err := m.getWakatimeFailureNotificationTemplate(WakatimeFailureNotificationNotificationTplData{
|
||||
PublicUrl: m.config.Server.PublicUrl,
|
||||
NumFailures: numFailures,
|
||||
})
|
||||
@ -84,7 +90,7 @@ func (m *MailService) SendWakatimeFailureNotification(recipient *models.User, nu
|
||||
}
|
||||
|
||||
func (m *MailService) SendImportNotification(recipient *models.User, duration time.Duration, numHeartbeats int) error {
|
||||
tpl, err := getImportNotificationTemplate(ImportNotificationTplData{
|
||||
tpl, err := m.getImportNotificationTemplate(ImportNotificationTplData{
|
||||
PublicUrl: m.config.Server.PublicUrl,
|
||||
Duration: fmt.Sprintf("%.0f seconds", duration.Seconds()),
|
||||
NumHeartbeats: numHeartbeats,
|
||||
@ -102,7 +108,7 @@ func (m *MailService) SendImportNotification(recipient *models.User, duration ti
|
||||
}
|
||||
|
||||
func (m *MailService) SendReport(recipient *models.User, report *models.Report) error {
|
||||
tpl, err := getReportTemplate(ReportTplData{report})
|
||||
tpl, err := m.getReportTemplate(ReportTplData{report})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -115,68 +121,38 @@ func (m *MailService) SendReport(recipient *models.User, report *models.Report)
|
||||
return m.sendingService.Send(mail)
|
||||
}
|
||||
|
||||
func getPasswordResetTemplate(data PasswordResetTplData) (*bytes.Buffer, error) {
|
||||
tpl, err := loadTemplate(tplNamePasswordReset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (m *MailService) getPasswordResetTemplate(data PasswordResetTplData) (*bytes.Buffer, error) {
|
||||
var rendered bytes.Buffer
|
||||
if err := tpl.Execute(&rendered, data); err != nil {
|
||||
if err := m.templates[m.fmtName(tplNamePasswordReset)].Execute(&rendered, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rendered, nil
|
||||
}
|
||||
|
||||
func getWakatimeFailureNotificationTemplate(data WakatimeFailureNotificationNotificationTplData) (*bytes.Buffer, error) {
|
||||
tpl, err := loadTemplate(tplNameWakatimeFailureNotification)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (m *MailService) getWakatimeFailureNotificationTemplate(data WakatimeFailureNotificationNotificationTplData) (*bytes.Buffer, error) {
|
||||
var rendered bytes.Buffer
|
||||
if err := tpl.Execute(&rendered, data); err != nil {
|
||||
if err := m.templates[m.fmtName(tplNameWakatimeFailureNotification)].Execute(&rendered, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rendered, nil
|
||||
}
|
||||
|
||||
func getImportNotificationTemplate(data ImportNotificationTplData) (*bytes.Buffer, error) {
|
||||
tpl, err := loadTemplate(tplNameImportNotification)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (m *MailService) getImportNotificationTemplate(data ImportNotificationTplData) (*bytes.Buffer, error) {
|
||||
var rendered bytes.Buffer
|
||||
if err := tpl.Execute(&rendered, data); err != nil {
|
||||
if err := m.templates[m.fmtName(tplNameImportNotification)].Execute(&rendered, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rendered, nil
|
||||
}
|
||||
|
||||
func getReportTemplate(data ReportTplData) (*bytes.Buffer, error) {
|
||||
tpl, err := loadTemplate(tplNameReport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (m *MailService) getReportTemplate(data ReportTplData) (*bytes.Buffer, error) {
|
||||
var rendered bytes.Buffer
|
||||
if err := tpl.Execute(&rendered, data); err != nil {
|
||||
if err := m.templates[m.fmtName(tplNameReport)].Execute(&rendered, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rendered, nil
|
||||
}
|
||||
|
||||
func loadTemplate(tplName string) (*template.Template, error) {
|
||||
tplFile, err := views.TemplateFiles.Open(fmt.Sprintf("mail/%s.tpl.html", tplName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tplFile.Close()
|
||||
|
||||
tplData, err := ioutil.ReadAll(tplFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return template.
|
||||
New(tplName).
|
||||
Funcs(routes.DefaultTemplateFuncs()).
|
||||
Parse(string(tplData))
|
||||
func (m *MailService) fmtName(name string) string {
|
||||
return fmt.Sprintf("%s.tpl.html", name)
|
||||
}
|
||||
|
@ -34,10 +34,10 @@ Chart.defaults.global.defaultFontColor = "#E2E8F0"
|
||||
Chart.defaults.global.defaultColor = "#E2E8F0"
|
||||
|
||||
String.prototype.toHHMMSS = function () {
|
||||
var sec_num = parseInt(this, 10)
|
||||
var hours = Math.floor(sec_num / 3600)
|
||||
var minutes = Math.floor((sec_num - (hours * 3600)) / 60)
|
||||
var seconds = sec_num - (hours * 3600) - (minutes * 60)
|
||||
const sec_num = parseInt(this, 10)
|
||||
let hours = Math.floor(sec_num / 3600)
|
||||
let minutes = Math.floor((sec_num - (hours * 3600)) / 60)
|
||||
let seconds = sec_num - (hours * 3600) - (minutes * 60)
|
||||
|
||||
if (hours < 10) {
|
||||
hours = '0' + hours
|
||||
@ -48,14 +48,14 @@ String.prototype.toHHMMSS = function () {
|
||||
if (seconds < 10) {
|
||||
seconds = '0' + seconds
|
||||
}
|
||||
return hours + ':' + minutes + ':' + seconds
|
||||
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
|
||||
return `${hours}:${minutes}`
|
||||
}
|
||||
|
||||
function draw(subselection) {
|
||||
@ -80,7 +80,7 @@ function draw(subselection) {
|
||||
.filter((c, i) => shouldUpdate(i))
|
||||
.forEach(c => c.destroy())
|
||||
|
||||
let projectChart = !projectsCanvas.classList.contains('hidden') && shouldUpdate(0)
|
||||
let projectChart = projectsCanvas && !projectsCanvas.classList.contains('hidden') && shouldUpdate(0)
|
||||
? new Chart(projectsCanvas.getContext('2d'), {
|
||||
type: 'horizontalBar',
|
||||
data: {
|
||||
@ -114,7 +114,10 @@ function draw(subselection) {
|
||||
xAxes: [{
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: 'Seconds'
|
||||
labelString: 'Duration (hh:mm:ss)'
|
||||
},
|
||||
ticks: {
|
||||
callback: (label) => label.toString().toHHMMSS()
|
||||
}
|
||||
}]
|
||||
},
|
||||
@ -125,7 +128,7 @@ function draw(subselection) {
|
||||
})
|
||||
: null
|
||||
|
||||
let osChart = !osCanvas.classList.contains('hidden') && shouldUpdate(1)
|
||||
let osChart = osCanvas && !osCanvas.classList.contains('hidden') && shouldUpdate(1)
|
||||
? new Chart(osCanvas.getContext('2d'), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
@ -158,7 +161,7 @@ function draw(subselection) {
|
||||
})
|
||||
: null
|
||||
|
||||
let editorChart = !editorsCanvas.classList.contains('hidden') && shouldUpdate(2)
|
||||
let editorChart = editorsCanvas && !editorsCanvas.classList.contains('hidden') && shouldUpdate(2)
|
||||
? new Chart(editorsCanvas.getContext('2d'), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
@ -191,7 +194,7 @@ function draw(subselection) {
|
||||
})
|
||||
: null
|
||||
|
||||
let languageChart = !languagesCanvas.classList.contains('hidden') && shouldUpdate(3)
|
||||
let languageChart = languagesCanvas && !languagesCanvas.classList.contains('hidden') && shouldUpdate(3)
|
||||
? new Chart(languagesCanvas.getContext('2d'), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
@ -224,7 +227,7 @@ function draw(subselection) {
|
||||
})
|
||||
: null
|
||||
|
||||
let machineChart = !machinesCanvas.classList.contains('hidden') && shouldUpdate(4)
|
||||
let machineChart = machinesCanvas && !machinesCanvas.classList.contains('hidden') && shouldUpdate(4)
|
||||
? new Chart(machinesCanvas.getContext('2d'), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
@ -257,7 +260,7 @@ function draw(subselection) {
|
||||
})
|
||||
: null
|
||||
|
||||
let labelChart = !labelsCanvas.classList.contains('hidden') && shouldUpdate(5)
|
||||
let labelChart = labelsCanvas && !labelsCanvas.classList.contains('hidden') && shouldUpdate(5)
|
||||
? new Chart(labelsCanvas.getContext('2d'), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
@ -305,9 +308,12 @@ function parseTopN() {
|
||||
}
|
||||
|
||||
function togglePlaceholders(mask) {
|
||||
const placeholderElements = containers.map(c => c.querySelector('.placeholder-container'))
|
||||
const placeholderElements = containers.map(c => c ? c.querySelector('.placeholder-container'): null)
|
||||
|
||||
for (let i = 0; i < mask.length; i++) {
|
||||
if (placeholderElements[i] === null) {
|
||||
continue;
|
||||
}
|
||||
if (!mask[i]) {
|
||||
canvases[i].classList.add('hidden')
|
||||
placeholderElements[i].classList.remove('hidden')
|
||||
|
BIN
static/assets/images/jetbrains-logo.png
Normal file
BIN
static/assets/images/jetbrains-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
@ -46,7 +46,7 @@ func Add(i, j int) int {
|
||||
}
|
||||
|
||||
func ParseUserAgent(ua string) (string, string, error) {
|
||||
re := regexp.MustCompile(`(?iU)^wakatime\/[\d+.]+\s\((\w+)-.*\)\s.+\s([^\/\s]+)-wakatime\/.+$`)
|
||||
re := regexp.MustCompile(`(?iU)^wakatime\/v?[\d+.]+\s\((\w+)-.*\)\s.+\s([^\/\s]+)-wakatime\/.+$`)
|
||||
groups := re.FindAllStringSubmatch(ua, -1)
|
||||
if len(groups) == 0 || len(groups[0]) != 3 {
|
||||
return "", "", errors.New("failed to parse user agent string")
|
||||
|
@ -37,6 +37,12 @@ func TestCommon_ParseUserAgent(t *testing.T) {
|
||||
"",
|
||||
errors.New(""),
|
||||
},
|
||||
{
|
||||
"wakatime/v1.18.11 (linux-5.13.8-200.fc34.x86_64-x86_64) go1.16.7 emacs-wakatime/1.0.2",
|
||||
"linux",
|
||||
"emacs",
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
@ -3,8 +3,13 @@ package utils
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
)
|
||||
|
||||
type TemplateMap map[string]*template.Template
|
||||
|
||||
func Json(data interface{}) template.JS {
|
||||
d, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
@ -19,3 +24,40 @@ func ToRunes(s string) (r []string) {
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func LoadTemplates(templateFs fs.FS, funcs template.FuncMap) (TemplateMap, error) {
|
||||
tpls := template.New("").Funcs(funcs)
|
||||
templates := make(map[string]*template.Template)
|
||||
|
||||
files, err := fs.ReadDir(templateFs, ".")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
tplName := file.Name()
|
||||
if file.IsDir() || path.Ext(tplName) != ".html" {
|
||||
continue
|
||||
}
|
||||
|
||||
templateFile, err := templateFs.Open(tplName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
templateData, err := ioutil.ReadAll(templateFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
templateFile.Close()
|
||||
|
||||
tpl, err := tpls.New(tplName).Parse(string(templateData))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
templates[tplName] = tpl
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
1.29.3
|
||||
1.29.5
|
||||
|
@ -23,13 +23,13 @@
|
||||
<div class="mb-8">
|
||||
<label class="inline-block text-sm mb-1 text-gray-500" for="username">Username</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="text" id="username"
|
||||
type="text" id="username" autocomplete="username"
|
||||
name="username" placeholder="Enter your username" minlength="1" required autofocus>
|
||||
</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"
|
||||
type="password" id="password"
|
||||
type="password" id="password" autocomplete="current-password"
|
||||
name="password" placeholder="******" minlength="6" required>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
|
88
views/mail/head.tpl.html
Normal file
88
views/mail/head.tpl.html
Normal file
@ -0,0 +1,88 @@
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Wakapi</title>
|
||||
<style>
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
.btn-primary table td:hover {
|
||||
background-color: #047857 !important;
|
||||
}
|
||||
.btn-primary a:hover {
|
||||
background-color: #047857 !important;
|
||||
border-color: #047857 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
@ -1,107 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Wakapi – Data Import Finished</title>
|
||||
<style>
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
.btn-primary table td:hover {
|
||||
background-color: #047857 !important;
|
||||
}
|
||||
.btn-primary a:hover {
|
||||
background-color: #047857 !important;
|
||||
border-color: #047857 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
{{ template "head.tpl.html" . }}
|
||||
|
||||
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
|
||||
<tr>
|
||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"> </td>
|
||||
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
|
||||
<div class="header" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
|
||||
<img src="https://wakapi.dev/assets/images/android-chrome-192x192.png?utm_source=mail" alt="Wakapi Logo" width="96" style="width: 96px">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{ template "theader.tpl.html" . }}
|
||||
|
||||
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
|
||||
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">
|
||||
@ -134,15 +41,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
|
||||
Powered by <a href="https://wakapi.dev" style="color: #999999; font-size: 12px; text-align: center; text-decoration: none;">Wakapi.dev</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{ template "tfooter.tpl.html" . }}
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"> </td>
|
||||
|
6
views/mail/mail.go
Normal file
6
views/mail/mail.go
Normal file
@ -0,0 +1,6 @@
|
||||
package mail
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed *.html
|
||||
var TemplateFiles embed.FS
|
@ -1,107 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Wakapi – Report</title>
|
||||
<style>
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
.btn-primary table td:hover {
|
||||
background-color: #047857 !important;
|
||||
}
|
||||
.btn-primary a:hover {
|
||||
background-color: #047857 !important;
|
||||
border-color: #047857 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
{{ template "head.tpl.html" . }}
|
||||
|
||||
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
|
||||
<tr>
|
||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"> </td>
|
||||
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
|
||||
<div class="header" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
|
||||
<img src="https://wakapi.dev/assets/images/android-chrome-192x192.png?utm_source=mail" alt="Wakapi Logo" width="96" style="width: 96px">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{ template "theader.tpl.html" . }}
|
||||
|
||||
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
|
||||
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">
|
||||
@ -180,15 +87,8 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
|
||||
Powered by <a href="https://wakapi.dev" style="color: #999999; font-size: 12px; text-align: center; text-decoration: none;">Wakapi.dev</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{ template "tfooter.tpl.html" . }}
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"> </td>
|
||||
|
@ -1,107 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Wakapi – Reset Password</title>
|
||||
<style>
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
.btn-primary table td:hover {
|
||||
background-color: #047857 !important;
|
||||
}
|
||||
.btn-primary a:hover {
|
||||
background-color: #047857 !important;
|
||||
border-color: #047857 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
{{ template "head.tpl.html" . }}
|
||||
|
||||
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
|
||||
<tr>
|
||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"> </td>
|
||||
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
|
||||
<div class="header" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
|
||||
<img src="https://wakapi.dev/assets/images/android-chrome-192x192.png?utm_source=mail" alt="Wakapi Logo" width="96" style="width: 96px">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{ template "theader.tpl.html" . }}
|
||||
|
||||
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
|
||||
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">
|
||||
@ -135,15 +42,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
|
||||
Powered by <a href="https://wakapi.dev" style="color: #999999; font-size: 12px; text-align: center; text-decoration: none;">Wakapi.dev</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{ template "tfooter.tpl.html" . }}
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"> </td>
|
||||
|
9
views/mail/tfooter.tpl.html
Normal file
9
views/mail/tfooter.tpl.html
Normal file
@ -0,0 +1,9 @@
|
||||
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
|
||||
Powered by <a href="https://wakapi.dev" style="color: #999999; font-size: 12px; text-align: center; text-decoration: none;">Wakapi.dev</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
9
views/mail/theader.tpl.html
Normal file
9
views/mail/theader.tpl.html
Normal file
@ -0,0 +1,9 @@
|
||||
<div class="header" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
|
||||
<img src="https://wakapi.dev/assets/images/android-chrome-192x192.png?utm_source=mail" alt="Wakapi Logo" width="96" style="width: 96px">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
@ -1,107 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>Wakapi – WakaTime Connection Failure</title>
|
||||
<style>
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
.btn-primary table td:hover {
|
||||
background-color: #047857 !important;
|
||||
}
|
||||
.btn-primary a:hover {
|
||||
background-color: #047857 !important;
|
||||
border-color: #047857 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
{{ template "head.tpl.html" . }}
|
||||
|
||||
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
|
||||
<tr>
|
||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"> </td>
|
||||
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
|
||||
<div class="header" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
|
||||
<img src="https://wakapi.dev/assets/images/android-chrome-192x192.png?utm_source=mail" alt="Wakapi Logo" width="96" style="width: 96px">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{ template "theader.tpl.html" . }}
|
||||
|
||||
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
|
||||
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">
|
||||
@ -134,15 +41,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
|
||||
Powered by <a href="https://wakapi.dev" style="color: #999999; font-size: 12px; text-align: center; text-decoration: none;">Wakapi.dev</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{{ template "tfooter.tpl.html" . }}
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"> </td>
|
||||
|
@ -232,12 +232,6 @@
|
||||
|
||||
{{ template "foot.tpl.html" . }}
|
||||
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
document.getElementById('api-key-instruction').innerHTML = document.getElementById('api-key-container').value
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
const languageColors = {{ .LanguageColors | json }}
|
||||
const editorColors = {{ .EditorColors | json }}
|
||||
@ -251,6 +245,7 @@
|
||||
wakapiData.machines = {{ .Machines | json }}
|
||||
wakapiData.labels = {{ .Labels | json }}
|
||||
|
||||
if (document.getElementById('to-date-picker') !== null) {
|
||||
document.getElementById("to-date-picker").onchange = function () {
|
||||
var input = document.getElementById("from-date-picker");
|
||||
input.setAttribute("max", this.value);
|
||||
@ -260,6 +255,9 @@
|
||||
var input = document.getElementById("to-date-picker");
|
||||
input.setAttribute("min", this.value);
|
||||
}
|
||||
} else {
|
||||
document.getElementById('api-key-instruction').innerHTML = document.getElementById('api-key-container').value
|
||||
}
|
||||
|
||||
</script>
|
||||
<script src="assets/app.js"></script>
|
||||
|
@ -2,5 +2,5 @@ package views
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed *.html mail/*.html
|
||||
//go:embed *.html
|
||||
var TemplateFiles embed.FS
|
||||
|
Reference in New Issue
Block a user