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 @@
+
\ 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
-
-
-
+
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 += `
`
+ if (srv['icon'] === null) {
+ html += `
`
+ } else {
+ html += `
`
+ }
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');