1
0
mirror of https://github.com/nxshock/gron.git synced 2024-11-27 03:41:00 +05:00

New updates

* Rework job status
* Group WebUI files
* New WebUI details page
This commit is contained in:
nxshock 2022-03-30 21:06:55 +05:00
parent 236507c5c0
commit 1c00dfabc9
9 changed files with 391 additions and 241 deletions

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
_ "embed" _ "embed"
"fmt" "fmt"
"io/fs"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -12,25 +13,27 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
//go:embed font.ttf
var font []byte
func httpServer(listenAddress string) { func httpServer(listenAddress string) {
if listenAddress == "none" { if listenAddress == "none" {
return return
} }
http.HandleFunc("/", handler) http.HandleFunc("/", handler)
http.HandleFunc("/font.ttf", handleFont)
http.HandleFunc("/reloadJobs", handleReloadJobs) http.HandleFunc("/reloadJobs", handleReloadJobs)
http.HandleFunc("/shutdown", handleShutdown) http.HandleFunc("/shutdown", handleShutdown)
http.HandleFunc("/start", handleForceStart) http.HandleFunc("/start", handleForceStart)
http.HandleFunc("/details", handleDetails)
log.WithField("job", "http_server").Fatal(http.ListenAndServe(listenAddress, nil)) log.WithField("job", "http_server").Fatal(http.ListenAndServe(listenAddress, nil))
} }
func handler(w http.ResponseWriter, r *http.Request) { func handler(w http.ResponseWriter, r *http.Request) {
if r.RequestURI != "/" { 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 return
} }
@ -43,7 +46,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
job.NextLaunch = jobEntry.Next.Format(config.TimeFormat) job.NextLaunch = jobEntry.Next.Format(config.TimeFormat)
jobs = append(jobs, job) jobs = append(jobs, job)
} }
indexTemplate.ExecuteTemplate(buf, "index", jobs) templates.ExecuteTemplate(buf, "index.htm", jobs)
globalMutex.RUnlock() globalMutex.RUnlock()
buf.WriteTo(w) buf.WriteTo(w)
@ -108,8 +111,25 @@ func handleReloadJobs(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
} }
func handleFont(w http.ResponseWriter, r *http.Request) { func handleDetails(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "font/ttf") jobName := r.FormValue("jobName")
w.Header().Add("Cache-Control", "public") // TODO if jobName == "" {
w.Write(font) 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)
} }

225
index.htm
View File

@ -1,225 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>gron</title>
<style>
/* 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;
font-size: 150%;
margin-bottom: 0.5em;
}
main {
flex-grow: 1;
padding-left: 1em;
padding-right: 1em;
}
table {
border-collapse: collapse;
}
th {
background-color: var(--base01);
}
td,
th {
border: 1px solid var(--base03);
padding: 0.25em;
}
td:first-child {
padding: 0;
}
table button {
width: 100%;
color: #fff;
background-color: var(--base01);
border: 0;
cursor: pointer;
padding: 0.25em;
}
form {
display: none;
}
pre {
font-family: "Consolas";
}
.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);
}
.red {
color: var(--base08);
}
</style>
</head>
<body>
<main>
<div class="dropdown">
<button class="dropbtn">☰ Menu</button>
<div class="dropdown-content">
<a href="/reloadJobs">&#x27f3; Reload jobs</a>
<a>
<hr>
</a>
<a href="/shutdown">&#x23fb; Shutdown</a>
</div>
</div>
<h1>Job list</h1>
<table>
<tr>
<th>Name</th>
<th>Description</th>
<th>Cron</th>
<th>Status</th>
<th>Start time</th>
<th>Finish time</th>
<th>Duration</th>
<th>Next launch</th>
<th>Last error</th>
</tr>
{{range .}}<tr>
<td>
<form action="/start" method="get" id="form-{{.Name}}"></form>
<button{{if gt .CurrentRunningCount 0}} class="runningbg" {{else}}{{if .LastError}} class="errorbg" {{end}}{{end}} type="submit" form="form-{{.Name}}" name="jobName" value="{{.Name}}" {{if gt .CurrentRunningCount 0}} disabled{{end}}>{{.Name}}</button>
</td>
<td class="smaller">{{.JobConfig.Description}}</td>
<td class="nowrap" align="right">
<pre>{{.JobConfig.Cron}}</pre>
</td>
<td class="nowrap">{{if gt .CurrentRunningCount 1}}<span class="red">&#x2bc1; running {{.CurrentRunningCount}} jobs</span>{{else}}{{if .CurrentRunningCount}}<span class="green">&#x2bc8; running</span>{{else}}&#x2bc0; inactive{{end}}{{end}}</td>
<td>{{.LastStartTime}}</td>
<td>{{.LastEndTime}}</td>
<td align="right">{{.LastExecutionDuration}}</td>
<td>{{.NextLaunch}}</td>
<td class="smaller red">{{.LastError}}</td>
</tr>{{end}}
</table>
</main>
</body>
</html>

9
job.go
View File

@ -30,6 +30,7 @@ type Job struct {
JobConfig JobConfig JobConfig JobConfig
// Fields for stats // Fields for stats
Status Status
CurrentRunningCount int CurrentRunningCount int
LastStartTime string LastStartTime string
LastEndTime string LastEndTime string
@ -50,6 +51,7 @@ func readJob(filePath string) (*Job, error) {
job := &Job{ job := &Job{
Name: strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)), Name: strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)),
Status: Inactive,
JobConfig: jobConfig} JobConfig: jobConfig}
return job, nil return job, nil
@ -100,6 +102,7 @@ func (j *Job) Run() {
globalMutex.Lock() globalMutex.Lock()
j.CurrentRunningCount++ j.CurrentRunningCount++
j.Status = Running
j.LastStartTime = startTime.Format(config.TimeFormat) j.LastStartTime = startTime.Format(config.TimeFormat)
globalMutex.Unlock() globalMutex.Unlock()
@ -111,12 +114,14 @@ func (j *Job) Run() {
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
j.Status = Error
log.Error(err.Error()) log.Error(err.Error())
globalMutex.Lock() globalMutex.Lock()
j.LastError = err.Error() j.LastError = err.Error()
globalMutex.Unlock() globalMutex.Unlock()
} else { } else {
j.Status = Inactive
globalMutex.Lock() globalMutex.Lock()
j.LastError = "" j.LastError = ""
globalMutex.Unlock() globalMutex.Unlock()
@ -141,7 +146,9 @@ func (j *Job) Run() {
if i == 0 { if i == 0 {
log.Printf("Job failed, restarting in %d seconds.", j.JobConfig.RestartSec) 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) log.Printf("Retry attempt №%d of %d failed, restarting in %d seconds.", i, j.JobConfig.NumberOfRestartAttemts, j.JobConfig.RestartSec)
} else { } else {
log.Printf("Retry attempt №%d of %d failed.", i, j.JobConfig.NumberOfRestartAttemts) log.Printf("Retry attempt №%d of %d failed.", i, j.JobConfig.NumberOfRestartAttemts)

10
status.go Normal file
View File

@ -0,0 +1,10 @@
package main
type Status int
const (
Inactive Status = iota
Running
Error
Restarting
)

View File

@ -1,20 +1,20 @@
package main package main
import ( import (
_ "embed" "embed"
"html/template" "html/template"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
//go:embed index.htm //go:embed webui/*
var indexTemplateStr string var siteFS embed.FS
var indexTemplate *template.Template var templates *template.Template
func initTemplate() { func initTemplate() {
var err error var err error
indexTemplate, err = template.New("index").Parse(indexTemplateStr) // TODO: optimize templates, err = template.ParseFS(siteFS, "webui/*.htm")
if err != nil { if err != nil {
log.Fatalln("init template error:", err) log.Fatalln("init template error:", err)
} }

78
webui/details.htm Normal file
View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>gron</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<main>
<a class="dropdown dropbtn" href="/" class="dropbtn">&#x2190; Back</a>
<h1>{{.Name}}</h1>
<table class="stats">
<tr>
<td>Description</td>
<td>{{.JobConfig.Description}}</td>
</tr>
<tr>
<td>Cron</td>
<td><pre>{{.JobConfig.Cron}}</pre></td>
</td>
<tr>
<td>Command</td>
<td><pre>{{.JobConfig.Command}}</pre></td>
</tr>
<tr>
<td>Status</td>
<td>{{if eq .Status 0}}&#x2bc0; inactive{{end}}{{if eq .Status 1}}<span class="green">&#x2bc8; running</span>{{end}}{{if eq .Status 2}}<span class="red">&#x2bc1; error</span>{{end}}{{if eq .Status 3}}<span class="orange">&#x27f3; restarting</span>{{end}}</td>
</tr>
<tr>
<td colspan="2">
<h2>Stats</h2>
</td>
</tr>
<tr>
<td>Start time</td>
<td>{{.LastStartTime}}</td>
</tr>
<tr>
<td>Finish time</td>
<td>{{.LastEndTime}}</td>
</tr>
<tr>
<td>Duration</td>
<td>{{.LastExecutionDuration}}</td>
</tr>
<tr>
<td>Last error</td>
<td><pre class="red">{{.LastError}}</pre></td>
</tr>
<tr>
<td>Next launch</td>
<td>{{.NextLaunch}}</td>
</tr>
<tr>
<td colspan="2">
<h2>On error action</h2>
</td>
</tr>
<tr>
<td>Restart rule</td>
<td>{{if eq .JobConfig.RestartRule 0}}no restart{{end}}{{if eq .JobConfig.RestartRule 1}}restart on error{{end}}</td>
</tr>
<tr>
<td>Number of restart attempts</td>
<td>{{.JobConfig.NumberOfRestartAttemts}}</td>
</tr>
<tr>
<td>Restart delay</td>
<td>{{if eq .JobConfig.RestartSec 0}}none{{else}}{{.JobConfig.RestartSec}} sec{{end}}</td>
</tr>
</table>
</main>
</body>
</html>

57
webui/index.htm Normal file
View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>gron</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<main>
<div class="dropdown">
<button class="dropbtn">☰ Menu</button>
<div class="dropdown-content">
<a href="/reloadJobs">&#x27f3; Reload jobs</a>
<a>
<hr>
</a>
<a href="/shutdown">&#x23fb; Shutdown</a>
</div>
</div>
<h1>Job list</h1>
<table>
<tr>
<th>Name</th>
<th>Description</th>
<th>Cron</th>
<th>Status</th>
<th>Start time</th>
<th>Finish time</th>
<th>Duration</th>
<th>Next launch</th>
<th>Details</th>
</tr>
{{range .}}
<tr>
<td class="no-padding">
<form action="/start" method="get" id="form-{{.Name}}"></form>
<button{{if gt .CurrentRunningCount 0}} class="runningbg" {{else}}{{if .LastError}} class="errorbg" {{end}}{{end}} type="submit" form="form-{{.Name}}" name="jobName" value="{{.Name}}" {{if gt .CurrentRunningCount 0}} disabled{{end}}>{{.Name}}</button>
</td>
<td class="smaller">{{.JobConfig.Description}}</td>
<td class="nowrap" align="right">
<pre>{{.JobConfig.Cron}}</pre>
</td>
<td class="nowrap">{{if eq .Status 0}}&#x2bc0; inactive{{end}}{{if eq .Status 1}}<span class="green">&#x2bc8; running</span>{{end}}{{if eq .Status 2}}<span class="red">&#x2bc1; error</span>{{end}}{{if eq .Status 3}}<span class="orange">&#x27f3; restarting</span>{{end}}</td>
<td>{{.LastStartTime}}</td>
<td>{{.LastEndTime}}</td>
<td align="right">{{.LastExecutionDuration}}</td>
<td>{{.NextLaunch}}</td>
<td class="centered"><a href="/details?jobName={{.Name}}">open</a></td>
</tr>{{end}}
</table>
</main>
</body>
</html>

203
webui/style.css Normal file
View File

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