diff --git a/httpserver.go b/httpserver.go
index e5f658e..4f4e581 100644
--- a/httpserver.go
+++ b/httpserver.go
@@ -4,6 +4,7 @@ import (
"bytes"
_ "embed"
"fmt"
+ "io/fs"
"net"
"net/http"
"os"
@@ -12,25 +13,27 @@ import (
log "github.com/sirupsen/logrus"
)
-//go:embed font.ttf
-var font []byte
-
func httpServer(listenAddress string) {
if listenAddress == "none" {
return
}
http.HandleFunc("/", handler)
- http.HandleFunc("/font.ttf", handleFont)
http.HandleFunc("/reloadJobs", handleReloadJobs)
http.HandleFunc("/shutdown", handleShutdown)
http.HandleFunc("/start", handleForceStart)
+ http.HandleFunc("/details", handleDetails)
log.WithField("job", "http_server").Fatal(http.ListenAndServe(listenAddress, nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
if r.RequestURI != "/" {
- http.Error(w, "", http.StatusNotFound)
+ fs, err := fs.Sub(siteFS, "webui")
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ http.FileServer(http.FS(fs)).ServeHTTP(w, r)
return
}
@@ -43,7 +46,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
job.NextLaunch = jobEntry.Next.Format(config.TimeFormat)
jobs = append(jobs, job)
}
- indexTemplate.ExecuteTemplate(buf, "index", jobs)
+ templates.ExecuteTemplate(buf, "index.htm", jobs)
globalMutex.RUnlock()
buf.WriteTo(w)
@@ -108,8 +111,25 @@ func handleReloadJobs(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
-func handleFont(w http.ResponseWriter, r *http.Request) {
- w.Header().Add("Content-Type", "font/ttf")
- w.Header().Add("Cache-Control", "public") // TODO
- w.Write(font)
+func handleDetails(w http.ResponseWriter, r *http.Request) {
+ jobName := r.FormValue("jobName")
+ if jobName == "" {
+ http.Error(w, "job name is not specified", http.StatusBadRequest)
+ return
+ }
+
+ jobEntries := c.Entries()
+
+ for _, jobEntry := range jobEntries {
+ job := jobEntry.Job.(*Job)
+ if job.Name == jobName {
+ err := templates.ExecuteTemplate(w, "details.htm", job)
+ if err != nil {
+ fmt.Println(err)
+ }
+ return
+ }
+ }
+
+ http.Error(w, fmt.Sprintf("there is no job with name %s", jobName), http.StatusBadRequest)
}
diff --git a/index.htm b/index.htm
deleted file mode 100644
index 904f18e..0000000
--- a/index.htm
+++ /dev/null
@@ -1,225 +0,0 @@
-
-
-
-
-
-
- gron
-
-
-
-
-
-
- Job list
-
-
- Name |
- Description |
- Cron |
- Status |
- Start time |
- Finish time |
- Duration |
- Next launch |
- Last error |
-
- {{range .}}
-
-
-
- |
- {{.JobConfig.Description}} |
-
- {{.JobConfig.Cron}}
- |
- {{if gt .CurrentRunningCount 1}}⯁ running {{.CurrentRunningCount}} jobs{{else}}{{if .CurrentRunningCount}}⯈ running{{else}}⯀ inactive{{end}}{{end}} |
- {{.LastStartTime}} |
- {{.LastEndTime}} |
- {{.LastExecutionDuration}} |
- {{.NextLaunch}} |
- {{.LastError}} |
-
{{end}}
-
-
-
-
-
diff --git a/job.go b/job.go
index faa8e3a..81317ce 100644
--- a/job.go
+++ b/job.go
@@ -30,6 +30,7 @@ type Job struct {
JobConfig JobConfig
// Fields for stats
+ Status Status
CurrentRunningCount int
LastStartTime string
LastEndTime string
@@ -50,6 +51,7 @@ func readJob(filePath string) (*Job, error) {
job := &Job{
Name: strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)),
+ Status: Inactive,
JobConfig: jobConfig}
return job, nil
@@ -100,6 +102,7 @@ func (j *Job) Run() {
globalMutex.Lock()
j.CurrentRunningCount++
+ j.Status = Running
j.LastStartTime = startTime.Format(config.TimeFormat)
globalMutex.Unlock()
@@ -111,12 +114,14 @@ func (j *Job) Run() {
err := cmd.Run()
if err != nil {
+ j.Status = Error
log.Error(err.Error())
globalMutex.Lock()
j.LastError = err.Error()
globalMutex.Unlock()
} else {
+ j.Status = Inactive
globalMutex.Lock()
j.LastError = ""
globalMutex.Unlock()
@@ -141,7 +146,9 @@ func (j *Job) Run() {
if i == 0 {
log.Printf("Job failed, restarting in %d seconds.", j.JobConfig.RestartSec)
- } else if i+1 < j.JobConfig.NumberOfRestartAttemts {
+ j.Status = Restarting
+ } else if i < j.JobConfig.NumberOfRestartAttemts {
+ j.Status = Restarting
log.Printf("Retry attempt №%d of %d failed, restarting in %d seconds.", i, j.JobConfig.NumberOfRestartAttemts, j.JobConfig.RestartSec)
} else {
log.Printf("Retry attempt №%d of %d failed.", i, j.JobConfig.NumberOfRestartAttemts)
diff --git a/status.go b/status.go
new file mode 100644
index 0000000..2157c29
--- /dev/null
+++ b/status.go
@@ -0,0 +1,10 @@
+package main
+
+type Status int
+
+const (
+ Inactive Status = iota
+ Running
+ Error
+ Restarting
+)
diff --git a/template.go b/template.go
index 2248b62..c009287 100644
--- a/template.go
+++ b/template.go
@@ -1,20 +1,20 @@
package main
import (
- _ "embed"
+ "embed"
"html/template"
log "github.com/sirupsen/logrus"
)
-//go:embed index.htm
-var indexTemplateStr string
+//go:embed webui/*
+var siteFS embed.FS
-var indexTemplate *template.Template
+var templates *template.Template
func initTemplate() {
var err error
- indexTemplate, err = template.New("index").Parse(indexTemplateStr) // TODO: optimize
+ templates, err = template.ParseFS(siteFS, "webui/*.htm")
if err != nil {
log.Fatalln("init template error:", err)
}
diff --git a/webui/details.htm b/webui/details.htm
new file mode 100644
index 0000000..6e39272
--- /dev/null
+++ b/webui/details.htm
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+ gron
+
+
+
+
+
+ ← Back
+ {{.Name}}
+
+
+ Description |
+ {{.JobConfig.Description}} |
+
+
+ Cron |
+ {{.JobConfig.Cron}} |
+
+
+ Command |
+ {{.JobConfig.Command}} |
+
+
+ Status |
+ {{if eq .Status 0}}⯀ inactive{{end}}{{if eq .Status 1}}⯈ running{{end}}{{if eq .Status 2}}⯁ error{{end}}{{if eq .Status 3}}⟳ restarting{{end}} |
+
+
+
+ Stats
+ |
+
+
+ Start time |
+ {{.LastStartTime}} |
+
+
+ Finish time |
+ {{.LastEndTime}} |
+
+
+ Duration |
+ {{.LastExecutionDuration}} |
+
+
+ Last error |
+ {{.LastError}} |
+
+
+ Next launch |
+ {{.NextLaunch}} |
+
+
+
+ On error action
+ |
+
+
+ Restart rule |
+ {{if eq .JobConfig.RestartRule 0}}no restart{{end}}{{if eq .JobConfig.RestartRule 1}}restart on error{{end}} |
+
+
+ Number of restart attempts |
+ {{.JobConfig.NumberOfRestartAttemts}} |
+
+
+ Restart delay |
+ {{if eq .JobConfig.RestartSec 0}}none{{else}}{{.JobConfig.RestartSec}} sec{{end}} |
+
+
+
+
+
+
\ No newline at end of file
diff --git a/font.ttf b/webui/font.ttf
similarity index 100%
rename from font.ttf
rename to webui/font.ttf
diff --git a/webui/index.htm b/webui/index.htm
new file mode 100644
index 0000000..9d7163c
--- /dev/null
+++ b/webui/index.htm
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+ gron
+
+
+
+
+
+
+ Job list
+
+
+ Name |
+ Description |
+ Cron |
+ Status |
+ Start time |
+ Finish time |
+ Duration |
+ Next launch |
+ Details |
+
+ {{range .}}
+
+
+
+
+ |
+ {{.JobConfig.Description}} |
+
+ {{.JobConfig.Cron}}
+ |
+ {{if eq .Status 0}}⯀ inactive{{end}}{{if eq .Status 1}}⯈ running{{end}}{{if eq .Status 2}}⯁ error{{end}}{{if eq .Status 3}}⟳ restarting{{end}} |
+ {{.LastStartTime}} |
+ {{.LastEndTime}} |
+ {{.LastExecutionDuration}} |
+ {{.NextLaunch}} |
+ open |
+
{{end}}
+
+
+
+
+
\ No newline at end of file
diff --git a/webui/style.css b/webui/style.css
new file mode 100644
index 0000000..4107a9f
--- /dev/null
+++ b/webui/style.css
@@ -0,0 +1,203 @@
+/* Based on base16 architecture for building themes
+ https://github.com/chriskempson/base16 */
+
+ :root {
+ /* Default Background */
+ --base00: #282c34;
+ /* Lighter Background */
+ --base01: #353b45;
+ /* Selection Background */
+ --base02: #3e4451;
+ /* Comments, Invisibles, Line Highlighting */
+ --base03: #545862;
+ /* Dark Foreground (Used for status bars) */
+ --base04: #565c64;
+ /* Default Foreground, Caret, Delimiters, Operators */
+ --base05: #abb2bf;
+ /* Light Foreground (Not often used) */
+ --base06: #b6bdca;
+ /* Light Background (Not often used) */
+ --base07: #c8ccd4;
+ /* Red */
+ --base08: #e06c75;
+ /* Orange */
+ --base09: #d19a66;
+ /* Yellow */
+ --base0A: #e5c07b;
+ /* Green */
+ --base0B: #98c379;
+ /* Aqua */
+ --base0C: #56b6c2;
+ /* Blue */
+ --base0D: #61afef;
+ /* Purple */
+ --base0E: #c678dd;
+ /* Dark red */
+ --base0F: #be5046;
+}
+
+@font-face {
+ font-family: Roboto;
+ src: url('/font.ttf');
+}
+
+* {
+ font-family: Roboto;
+ font-size: 14px;
+ color: var(--base05);
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ height: 100%
+}
+
+body {
+ background-color: var(--base00);
+ display: flex;
+ justify-content: space-between;
+ flex-wrap: nowrap;
+ flex-direction: column;
+ height: 100%;
+}
+
+h1 {
+ margin-top: 1em;
+ margin-bottom: 0.5em;
+ font-size: 150%;
+}
+
+h2 {
+ margin-top: 1em;
+ margin-bottom: 0.5em;
+ font-size: 125%;
+}
+
+h1,
+h2 {
+ color: var(--base0D);
+ font-weight: bold;
+}
+
+main {
+ flex-grow: 1;
+ padding-left: 1em;
+ padding-right: 1em;
+}
+
+a {
+ text-decoration: none;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+th {
+ background-color: var(--base01);
+}
+
+td,
+th {
+ border: 1px solid var(--base01);
+ padding: 0.25em;
+}
+
+table button {
+ width: 100%;
+ color: #fff;
+ background-color: var(--base01);
+ border: 0;
+ cursor: pointer;
+ padding: 0.25em;
+}
+
+form {
+ display: none;
+}
+
+pre {
+ font-family: "Consolas";
+}
+
+.no-padding {
+ padding: 0;
+}
+
+.centered {
+ text-align: center;
+}
+
+.smaller {
+ font-size: 80%;
+}
+
+.nowrap {
+ white-space: nowrap;
+}
+
+.errorbg {
+ background-color: var(--base08);
+}
+
+.runningbg {
+ background-color: var(--base0B);
+}
+
+.dropbtn {
+ background-color: var(--base01);
+ color: var(--base05);
+ padding: 0.5em;
+ cursor: pointer;
+ border: 0;
+}
+
+.dropdown {
+ position: relative;
+ display: inline-block;
+ float: right;
+}
+
+.dropdown-content {
+ display: none;
+ position: absolute;
+ right: 0;
+ background-color: var(--base01);
+ z-index: 1;
+}
+
+.dropdown-content a {
+ color: var(--base05);
+ padding: 0.5em;
+ text-decoration: none;
+ display: block;
+ white-space: nowrap;
+}
+
+.dropdown:hover .dropdown-content {
+ display: block;
+}
+
+.green {
+ color: var(--base0B);
+}
+
+.orange {
+ color: var(--base09);
+}
+
+.red {
+ color: var(--base08);
+}
+
+table.stats td {
+ border-top: 1px solid var(--base01);
+ border-bottom: 1px solid var(--base01);
+ border-left: 0;
+ border-right: 0;
+}
+
+table.stats td:first-child {
+ font-weight: bold;
+}
\ No newline at end of file