diff --git a/.gitignore b/.gitignore index 6b231b7..624d6d5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ _build .run-data _run-data +_release + DOCKER_GIT_INFO .swaggobin diff --git a/.idea/golinter.xml b/.idea/golinter.xml new file mode 100644 index 0000000..084cb03 --- /dev/null +++ b/.idea/golinter.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Makefile b/Makefile index 70ce9d0..7e53fdd 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,18 @@ build: enums ids swagger fmt rm -f ./_build/bunny_backend go build -v -buildvcs=false -o _build/bunny_backend ./cmd/server +release: build + mkdir -p _release + rm -f ./_release/bunny + go build -v -buildvcs=false -o _release/bunny ./cmd/server + +restart: + mkdir -p _release + sudo rm -f ./_release/bunny + sudo systemctl stop localhostbunny + go build -v -buildvcs=false -o _release/bunny ./cmd/server + sudo systemctl start localhostbunny + enums: go generate models/enums.go diff --git a/api/handler/apiHandler.go b/api/handler/apiHandler.go index 677cd17..6b3d9f8 100644 --- a/api/handler/apiHandler.go +++ b/api/handler/apiHandler.go @@ -1,6 +1,7 @@ package handler import ( + "gogs.mikescher.com/BlackForestBytes/goext/exerr" "gogs.mikescher.com/BlackForestBytes/goext/ginext" bunny "locbunny" "locbunny/logic" @@ -45,3 +46,33 @@ func (h APIHandler) ListServer(pctx ginext.PreContext) ginext.HTTPResponse { return ginext.JSON(http.StatusOK, response{Servers: srvs}) } + +// GetIcon swaggerdoc +// +// @Summary Get Icon +// +// @Param cs path number true "Icon Checksum" +// +// @Router /icon/:cs [GET] +func (h APIHandler) GetIcon(pctx ginext.PreContext) ginext.HTTPResponse { + type uri struct { + Checksum string `uri:"cs"` + } + + var u uri + ctx, _, errResp := pctx.URI(&u).Start() + if errResp != nil { + return *errResp + } + defer ctx.Cancel() + + icn := h.app.GetIcon(ctx, u.Checksum) + if icn == nil { + return ginext.Error(exerr.New(bunny.ErrEntityNotFound, "Icon not found").Str("cs", u.Checksum).WithStatuscode(404).Build()) + } + + return ginext.Data(200, icn.ContentType, icn.Data). + WithHeader("X-BUNNY-ICONID", icn.IconID.String()). + WithHeader("X-BUNNY-CHECKSUM", icn.Checksum). + WithHeader("X-BUNNY-ICONDATE", icn.Time.String()) +} diff --git a/api/router.go b/api/router.go index 70afc34..d1278c9 100644 --- a/api/router.go +++ b/api/router.go @@ -64,6 +64,7 @@ func (r *Router) Init(e *ginext.GinWrapper) { // ================ API ================ api.GET("/server").Handle(r.apiHandler.ListServer) + api.GET("/icon/:cs").Handle(r.apiHandler.GetIcon) // ================ ================ diff --git a/go.mod b/go.mod index bcbd436..f376e3f 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,8 @@ require ( gogs.mikescher.com/BlackForestBytes/goext v0.0.288 ) +require github.com/adampresley/gofavigrab v0.0.0-20150913222647-3e339572468d // indirect + require ( github.com/go-ole/go-ole v1.2.6 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect diff --git a/go.sum b/go.sum index 3a3e747..34e3571 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/adampresley/gofavigrab v0.0.0-20150913222647-3e339572468d h1:he2p51oHkafDBlMOjLoCzPd1zJSZuQGseI3WmbQoLaY= +github.com/adampresley/gofavigrab v0.0.0-20150913222647-3e339572468d/go.mod h1:383ML3lBZdTp1YUk6jGiN6qw7duOG6aF6Y6OVn2uM50= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= diff --git a/icons/README.md b/icons/README.md new file mode 100644 index 0000000..45fcba9 --- /dev/null +++ b/icons/README.md @@ -0,0 +1,5 @@ + + + +// source: https://github.com/PapirusDevelopmentTeam/papirus-icon-theme + https://www.iconarchive.com/show/papirus-apps-icons-by-papirus-team.html \ No newline at end of file diff --git a/icons/androidstudio.svg b/icons/androidstudio.svg new file mode 100644 index 0000000..1d8e786 --- /dev/null +++ b/icons/androidstudio.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/icons/cups.svg b/icons/cups.svg new file mode 100644 index 0000000..e3cf98a --- /dev/null +++ b/icons/cups.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/icons/docker.svg b/icons/docker.svg new file mode 100644 index 0000000..b59fe27 --- /dev/null +++ b/icons/docker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/goland.svg b/icons/goland.svg new file mode 100644 index 0000000..a0f1c0b --- /dev/null +++ b/icons/goland.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/icons/icons.go b/icons/icons.go new file mode 100644 index 0000000..2d38b06 --- /dev/null +++ b/icons/icons.go @@ -0,0 +1,39 @@ +package icons + +import _ "embed" + +//go:embed androidstudio.svg +var AndroidStudio []byte + +//go:embed cups.svg +var CUPS []byte + +//go:embed docker.svg +var Docker []byte + +//go:embed goland.svg +var GoLand []byte + +//go:embed idea.svg +var IntellijIDEA []byte + +//go:embed java.svg +var Java []byte + +//go:embed mongo.svg +var MongoDB []byte + +//go:embed phpstorm.svg +var PHPStorm []byte + +//go:embed rider.svg +var Rider []byte + +//go:embed vlc.svg +var VLC []byte + +//go:embed webstorm.svg +var WebStorm []byte + +//go:embed pycharm.svg +var PyCharm []byte diff --git a/icons/idea.svg b/icons/idea.svg new file mode 100644 index 0000000..2442331 --- /dev/null +++ b/icons/idea.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/icons/java.svg b/icons/java.svg new file mode 100644 index 0000000..6e0e21e --- /dev/null +++ b/icons/java.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/mongo.svg b/icons/mongo.svg new file mode 100644 index 0000000..4c9f07f --- /dev/null +++ b/icons/mongo.svg @@ -0,0 +1 @@ +MongoDB \ No newline at end of file diff --git a/icons/phpstorm.svg b/icons/phpstorm.svg new file mode 100644 index 0000000..4047a06 --- /dev/null +++ b/icons/phpstorm.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/icons/pycharm.svg b/icons/pycharm.svg new file mode 100644 index 0000000..ea5c0fb --- /dev/null +++ b/icons/pycharm.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/icons/rider.svg b/icons/rider.svg new file mode 100644 index 0000000..3a1f7b1 --- /dev/null +++ b/icons/rider.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/icons/vlc.svg b/icons/vlc.svg new file mode 100644 index 0000000..7339a90 --- /dev/null +++ b/icons/vlc.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/icons/webstorm.svg b/icons/webstorm.svg new file mode 100644 index 0000000..2c96d0d --- /dev/null +++ b/icons/webstorm.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/logic/application.go b/logic/application.go index 9c64cd4..a07cb77 100644 --- a/logic/application.go +++ b/logic/application.go @@ -4,19 +4,24 @@ import ( "context" "errors" "fmt" + "github.com/adampresley/gofavigrab/parser" "github.com/cakturk/go-netstat/netstat" "github.com/rs/zerolog/log" "github.com/shirou/gopsutil/v3/process" + "gogs.mikescher.com/BlackForestBytes/goext/cryptext" "gogs.mikescher.com/BlackForestBytes/goext/ginext" "gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/rext" + "gogs.mikescher.com/BlackForestBytes/goext/rfctime" "gogs.mikescher.com/BlackForestBytes/goext/syncext" "io" bunny "locbunny" + "locbunny/icons" "locbunny/models" "locbunny/webassets" "net" "net/http" + "net/url" "os" "os/signal" "regexp" @@ -43,6 +48,9 @@ type Application struct { cacheLock sync.Mutex serverCacheValue []models.Server serverCacheTime *time.Time + + iconCache map[string]models.Icon + iconCacheLock sync.Mutex } func NewApp(ass *webassets.Assets) *Application { @@ -51,6 +59,7 @@ func NewApp(ass *webassets.Assets) *Application { Assets: ass, stopChan: make(chan bool), IsRunning: syncext.NewAtomicBool(false), + iconCache: make(map[string]models.Icon, 1024), } } @@ -258,29 +267,13 @@ func (app *Application) verifyHTTPConn(sock netstat.SockTabEntry, proto string, return models.Server{}, errors.New("skip self") } - c := http.Client{} - url := fmt.Sprintf("%s://localhost:%d", strings.ToLower(proto), port) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + resbody, header, statuscode, err := app.doRequest(ctx, proto, port, "") if err != nil { - log.Debug().Msg(fmt.Sprintf("Failed to create [%s|%s|%d] request to %d (-> %s)", strings.ToUpper(proto), ipversion, port, port, err.Error())) + log.Debug().Msg(fmt.Sprintf("Failed to [%s|%s|%d] request to %d (-> %s)", strings.ToUpper(proto), ipversion, port, port, err.Error())) return models.Server{}, err } - resp1, err := c.Do(req) - if err != nil { - log.Debug().Msg(fmt.Sprintf("Failed to send [%s|%s|%d] request to %s (-> %s)", strings.ToUpper(proto), ipversion, port, url, err.Error())) - return models.Server{}, err - } - - defer func() { _ = resp1.Body.Close() }() - - resbody, err := io.ReadAll(resp1.Body) - if err != nil { - log.Debug().Msg(fmt.Sprintf("Failed to read [%s|%s|%d] response from %s (-> %s)", strings.ToUpper(proto), ipversion, port, url, err.Error())) - return models.Server{}, err - } - - ct := resp1.Header.Get("Content-Type") + ct := header.Get("Content-Type") if ct != "" { var pnm *string = nil @@ -292,12 +285,21 @@ func (app *Application) verifyHTTPConn(sock netstat.SockTabEntry, proto string, name := app.DetectName(sock, ct, string(resbody)) + var iconRef *string = nil + iconData, iconCT := app.DetectIcon(sock, proto, port, name, string(resbody)) + if iconData != nil && iconCT != "" { + cs := cryptext.StrSha256(cryptext.BytesSha256(iconData) + iconCT) + _, _ = app.StoreIcon(cs, iconData, iconCT) + iconRef = &cs + } + return models.Server{ Port: port, IP: sock.LocalAddr.IP.String(), Name: name, + Icon: iconRef, Protocol: proto, - StatusCode: resp1.StatusCode, + StatusCode: statuscode, Response: string(resbody), ContentType: ct, Process: pnm, @@ -307,11 +309,33 @@ func (app *Application) verifyHTTPConn(sock netstat.SockTabEntry, proto string, }, nil } - log.Debug().Msg(fmt.Sprintf("Failed to categorize [%s|%s|%d] response from %s (Content-Type: '%s')", strings.ToUpper(proto), ipversion, port, url, ct)) + log.Debug().Msg(fmt.Sprintf("Failed to categorize [%s|%s|%d] response (Content-Type: '%s')", strings.ToUpper(proto), ipversion, port, ct)) return models.Server{}, errors.New("invalid response-type") } +func (app *Application) doRequest(ctx context.Context, proto string, port int, path string) ([]byte, http.Header, int, error) { + c := http.Client{} + url := fmt.Sprintf("%s://localhost:%d"+path, strings.ToLower(proto), port) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, nil, 0, err + } + + resp1, err := c.Do(req) + if err != nil { + return nil, nil, 0, err + } + + defer func() { _ = resp1.Body.Close() }() + + resbody, err := io.ReadAll(resp1.Body) + if err != nil { + return nil, nil, 0, err + } + return resbody, resp1.Header, resp1.StatusCode, nil +} + func (app *Application) DetectName(sock netstat.SockTabEntry, ct string, body string) string { if strings.Contains(strings.ToLower(ct), "html") { @@ -353,6 +377,103 @@ func (app *Application) DetectName(sock netstat.SockTabEntry, ct string, body st return "unknown" } +func (app *Application) DetectIcon(sock netstat.SockTabEntry, proto string, port int, name string, body string) ([]byte, string) { + + if strings.Contains(strings.ToLower(body), "it looks like you are trying to access mongodb over http on the native driver port.") { + return icons.MongoDB, "image/svg+xml" + } + + if sock.Process != nil { + pname := strings.ToLower(strings.TrimSpace(sock.Process.Name)) + if pname == "vlc" { + return icons.VLC, "image/svg+xml" + } + if pname == "cupsd" { + return icons.CUPS, "image/svg+xml" + } + if pname == "containerd" { + return icons.Docker, "image/svg+xml" + } + } + + name = strings.ToLower(name) + + if strings.HasPrefix(name, "goland") { + return icons.GoLand, "image/svg+xml" + } + if strings.HasPrefix(name, "phpstorm") { + return icons.PHPStorm, "image/svg+xml" + } + if strings.HasPrefix(name, "pycharm") { + return icons.PyCharm, "image/svg+xml" + } + if strings.HasPrefix(name, "webstorm") { + return icons.WebStorm, "image/svg+xml" + } + if strings.HasPrefix(name, "intellijidea") { + return icons.IntellijIDEA, "image/svg+xml" + } + if strings.HasPrefix(name, "rider") { + return icons.Rider, "image/svg+xml" + } + if strings.HasPrefix(name, "androidstudio") { + return icons.AndroidStudio, "image/svg+xml" + } + + if favurlAbs, err := parser.NewHTMLParser(body).GetFaviconURL(); err == nil { + if parsedURL, err := url.Parse(favurlAbs); err == nil { + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + resbody, hdr, sc, err := app.doRequest(ctx, proto, port, parsedURL.EscapedPath()) + if err == nil && sc >= 200 && sc < 300 && hdr.Get("Content-Type") != "" { + return resbody, hdr.Get("Content-Type") + } + } + } + + { + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + resbody, _, sc, err := app.doRequest(ctx, proto, port, "/favicon.ico") + if err == nil && sc >= 200 && sc < 300 { + return resbody, "image/vnd.microsoft.icon" + } + } + + { + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + resbody, _, sc, err := app.doRequest(ctx, proto, port, "/favicon.png") + if err == nil && sc >= 200 && sc < 300 { + return resbody, "image/png" + } + } + + { + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + resbody, _, sc, err := app.doRequest(ctx, proto, port, "/favicon.jpeg") + if err == nil && sc >= 200 && sc < 300 { + return resbody, "image/jpeg" + } + } + + { + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + resbody, _, sc, err := app.doRequest(ctx, proto, port, "/favicon.jpg") + if err == nil && sc >= 200 && sc < 300 { + return resbody, "image/jpeg" + } + } + + if sock.Process != nil && sock.Process.Name == "java" { + return icons.Java, "image/svg+xml" + } + + return nil, "" +} + func (app *Application) isInvalidHTMLTitle(title string) bool { title = strings.ToLower(title) title = strings.TrimSpace(title) @@ -423,3 +544,35 @@ func (app *Application) extractNameFromJava(cmdl []string) (string, bool) { return "", false } + +func (app *Application) StoreIcon(cs string, data []byte, ct string) (models.Icon, bool) { + app.iconCacheLock.Lock() + defer app.iconCacheLock.Unlock() + + if v, ok := app.iconCache[cs]; ok { + return v, false + } + + v := models.Icon{ + IconID: models.NewIconID(), + Checksum: cs, + Data: data, + ContentType: ct, + Time: rfctime.NowRFC3339Nano(), + } + + app.iconCache[cs] = v + + return v, true +} + +func (app *Application) GetIcon(ctx *ginext.AppContext, cs string) *models.Icon { + app.iconCacheLock.Lock() + defer app.iconCacheLock.Unlock() + + if v, ok := app.iconCache[cs]; ok { + return langext.Ptr(v) + } + + return nil +} diff --git a/models/icon.go b/models/icon.go new file mode 100644 index 0000000..7a2c64e --- /dev/null +++ b/models/icon.go @@ -0,0 +1,13 @@ +package models + +import ( + "gogs.mikescher.com/BlackForestBytes/goext/rfctime" +) + +type Icon struct { + IconID IconID `bson:"_id,omitempty" json:"id"` + Checksum string `bson:"checksum" json:"checksum"` + Data []byte `bson:"data" json:"data"` + ContentType string `bson:"contentType" json:"contentType"` + Time rfctime.RFC3339NanoTime `bson:"time" json:"time"` +} diff --git a/models/ids.go b/models/ids.go index 20e3ddb..ab16169 100644 --- a/models/ids.go +++ b/models/ids.go @@ -19,3 +19,5 @@ type AnyID string //@id:type type JobLogID string //@id:type type JobExecutionID string //@id:type + +type IconID string //@id:type diff --git a/models/ids_gen.go b/models/ids_gen.go index b612616..82efb4d 100644 --- a/models/ids_gen.go +++ b/models/ids_gen.go @@ -7,7 +7,7 @@ import "go.mongodb.org/mongo-driver/bson/bsontype" import "go.mongodb.org/mongo-driver/bson/primitive" import "gogs.mikescher.com/BlackForestBytes/goext/exerr" -const ChecksumIDGenerator = "c6ecd0c3665e4ed6d1316ccf2899ba29a28d9d06b6c6b97a86adc19c1343787e" // GoExtVersion: 0.0.288 +const ChecksumIDGenerator = "ce868e40b2314fd3d5484f0c1366d2580d188535e6d7400b16977f76c4c159a6" // GoExtVersion: 0.0.288 // ================================ AnyID (ids.go) ================================ @@ -101,3 +101,34 @@ func (i JobExecutionID) AsAny() AnyID { func NewJobExecutionID() JobExecutionID { return JobExecutionID(primitive.NewObjectID().Hex()) } + +// ================================ IconID (ids.go) ================================ + +func (i IconID) MarshalBSONValue() (bsontype.Type, []byte, error) { + if objId, err := primitive.ObjectIDFromHex(string(i)); err == nil { + return bson.MarshalValue(objId) + } else { + return 0, nil, exerr.New(exerr.TypeMarshalEntityID, "Failed to marshal IconID("+i.String()+") to ObjectId").Str("value", string(i)).Type("type", i).Build() + } +} + +func (i IconID) String() string { + return string(i) +} + +func (i IconID) ObjID() (primitive.ObjectID, error) { + return primitive.ObjectIDFromHex(string(i)) +} + +func (i IconID) Valid() bool { + _, err := primitive.ObjectIDFromHex(string(i)) + return err == nil +} + +func (i IconID) AsAny() AnyID { + return AnyID(i) +} + +func NewIconID() IconID { + return IconID(primitive.NewObjectID().Hex()) +} diff --git a/models/server.go b/models/server.go index 3673a58..c512609 100644 --- a/models/server.go +++ b/models/server.go @@ -12,4 +12,5 @@ type Server struct { UID uint32 `json:"uid"` SockState string `json:"sockState"` Name string `json:"name"` + Icon *string `json:"icon"` } diff --git a/swagger/swagger.json b/swagger/swagger.json index 460f91a..57a2c38 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -175,6 +175,21 @@ } } }, + "/icon/:cs": { + "get": { + "summary": "Get Icon", + "parameters": [ + { + "type": "number", + "description": "Icon Checksum", + "name": "cs", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, "/index.html": { "get": { "summary": "(Website)", @@ -304,6 +319,9 @@ "contentType": { "type": "string" }, + "icon": { + "type": "string" + }, "ip": { "type": "string" }, diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index d0561a7..55b7476 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -58,6 +58,8 @@ definitions: properties: contentType: type: string + icon: + type: string ip: type: string name: @@ -193,6 +195,16 @@ paths: summary: Return 200 after x seconds tags: - Common + /icon/:cs: + get: + parameters: + - description: Icon Checksum + in: path + name: cs + required: true + type: number + responses: {} + summary: Get Icon /index.html: get: responses: {} diff --git a/webassets/css/styles.css b/webassets/css/styles.css index b1e119c..6c2c125 100644 --- a/webassets/css/styles.css +++ b/webassets/css/styles.css @@ -18,7 +18,7 @@ body { main { display: grid; - grid-template-columns: auto 1fr auto; + grid-template-columns: 20px 50px 1fr 50px 20px; grid-column-gap: 1rem; grid-row-gap: 1rem; @@ -30,6 +30,14 @@ h1 { text-shadow: 0 0 8px #888; } +.header { + grid-column: 2/5; + + display: grid; + grid-template-columns: auto 1fr auto; + grid-column-gap: 1rem; +} + .loader_left, .loader_right{ display: flex; @@ -38,14 +46,39 @@ h1 { align-items: center; } +.loader_left img { + width: 24px; + height: 24px; + + margin-bottom: 4px; + + cursor: pointer; + transition: transform 0.15s ease-in-out, + opacity 0.10s ease-in-out; + + opacity: 0; +} + +.loader_left img:hover { + transform: scale(1.2, 1.2); + opacity: 1.0; +} + +.header:hover .loader_left img { + opacity: 0.5; +} + +.header:hover .loader_left img:hover { + opacity: 1.0; +} + + #maincontent { display: flex; flex-direction: column; gap: 0.5rem; - padding: 0 2rem; - - grid-column: 2; + grid-column: 1/-1; } .server { @@ -71,10 +104,18 @@ h1 { display: flex; justify-content: center; align-items: center; + + text-align: left; } -.server .txt_icon { - text-align: left; +.server .txt_icon img { + width: 16px; + height: 16px; + object-fit: contain; +} + +.server:not(:hover) .txt_icon img { + filter: grayscale(1) } .server .txt_port { diff --git a/webassets/icons/reload.svg b/webassets/icons/reload.svg new file mode 100644 index 0000000..3d7bcb5 --- /dev/null +++ b/webassets/icons/reload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webassets/index.html b/webassets/index.html index 4086609..ebecbf9 100644 --- a/webassets/index.html +++ b/webassets/index.html @@ -30,11 +30,11 @@
- -

LocalHostBunny

- - - +
+ reload +

LocalHostBunny

+ +
diff --git a/webassets/scripts/script.js b/webassets/scripts/script.js index e85e755..314f1d1 100644 --- a/webassets/scripts/script.js +++ b/webassets/scripts/script.js @@ -7,6 +7,8 @@ function now() { return (new Date()).getTime(); } function enc(v) { return `${v}`.replace(/[\u00A0-\u9999<>&]/g, i => '&#'+i.charCodeAt(0)+';') } +function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } + function updateHTML(servers) { const main = document.getElementById('maincontent') @@ -16,21 +18,30 @@ function updateHTML(servers) { for (const srv of servers) { html += ``; - html += `process` + if (srv['icon'] === null) { + html += `icon` + } else { + html += `icon` + } html += `${enc(srv['name'])}` html += `${enc(srv['port'])}` html += ``; } main.innerHTML = html; - } function onVisibilityChange() { - if (!document.hidden && (now() - last_refresh) > refresh_delay) autoReload(); + console.log('[I] Visibility changed to ' + document.hidden) + + if (!document.hidden && (now() - last_refresh) > refresh_delay) { + sleep(300).then(async () => await autoReload()); + } } async function autoReload() { + console.log('[I] AutoReload') + try { document.getElementById('loader').classList.remove('hidden');