Compare commits

..

8 commits

Author SHA1 Message Date
bc2df3accf add logo, reboot notice
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-21 15:44:10 +01:00
51eb9c0cac finished uploader component
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-21 01:31:38 +01:00
191d74076e Fix api urls for prod 2021-11-20 20:35:02 +01:00
2f893e458c Add uploader component 2021-11-20 20:32:29 +01:00
75a3d67402 ci: verbose golanci output
All checks were successful
continuous-integration/drone/push Build is passing
2021-11-19 20:47:24 +01:00
92fc0b057e update ci: download go mods before
Some checks failed
continuous-integration/drone/push Build is failing
2021-11-19 20:43:30 +01:00
14a4d7fef4 reordered CI steps
Some checks failed
continuous-integration/drone/push Build is failing
2021-11-19 20:36:57 +01:00
1f8546cb37 Add preact framework
Some checks failed
continuous-integration/drone Build is failing
2021-11-19 20:31:33 +01:00
49 changed files with 2631 additions and 154 deletions

32
.air.toml Normal file
View file

@ -0,0 +1,32 @@
root = "./src"
tmp_dir = "tmp"
[build]
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./src/."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor"]
exclude_file = []
exclude_regex = []
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
time = false
[misc]
clean_on_exit = false

18
.drone.yml Normal file
View file

@ -0,0 +1,18 @@
kind: pipeline
name: default
type: docker
steps:
- name: Frontend build
image: node:16-alpine
commands:
- cd ui
- npm install -g pnpm
- pnpm install
- pnpm run build
- name: Backend test
image: golangci/golangci-lint:latest
commands:
- go mod download
- golangci-lint run --timeout 5m
- go test -v ./src/...

View file

@ -1,17 +1,15 @@
[*] [*]
charset = utf-8 charset = utf-8
indent_style = space indent_style = tab
indent_size = 4 indent_size = 4
end_of_line = lf end_of_line = lf
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
max_line_length = 88 max_line_length = 88
[*.go] [*.py]
indent_style = tab indent_style = space
[Makefile]
indent_style = tab
[*.{json,md,rst,ini,yml,yaml}] [*.{json,md,rst,ini,yml,yaml}]
indent_style = space
indent_size = 2 indent_size = 2

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
tmp

View file

@ -1,8 +1,22 @@
repos: repos:
- repo: https://github.com/tekwizely/pre-commit-golang - repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-beta.4 rev: v1.0.0-beta.4
hooks: hooks:
- id: golangci-lint-repo-mod - id: golangci-lint-repo-mod
name: GolangCI Lint name: GolangCI Lint
- id: go-test-repo-mod - id: go-test-repo-mod
name: Test name: Backend tests
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.4.1
hooks:
- id: prettier
- repo: local
hooks:
- id: tsc
name: tsc
entry: tsc
language: node
files: \.tsx?$
args: ["-p", "./ui/tsconfig.json"]
additional_dependencies: ["typescript@4.5.2"]
pass_filenames: false

2
.prettierignore Normal file
View file

@ -0,0 +1,2 @@
node_modules
dist

15
.prettierrc.json Normal file
View file

@ -0,0 +1,15 @@
{
"useTabs": true,
"tabWidth": 4,
"semi": false,
"bracketSpacing": false,
"overrides": [
{
"files": ["*.json", "*.md", "*.rst", "*.ini", "*.yml", "*.yaml"],
"options": {
"useTabs": false,
"tabWidth": 2
}
}
]
}

29
.vscode/launch.json vendored
View file

@ -1,12 +1,21 @@
{ {
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Launch SEBRAUC server", "name": "SEBRAUC server",
"type": "go", "type": "go",
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"program": "${workspaceFolder}/src" "program": "${workspaceFolder}/src"
} },
] {
"name": "UI dev server",
"request": "launch",
"cwd": "${workspaceFolder}/ui",
"runtimeArgs": ["run-script", "dev"],
"runtimeExecutable": "npm",
"skipFiles": ["<node_internals>/**"],
"type": "pwa-node"
}
]
} }

1
README.md Normal file
View file

@ -0,0 +1 @@
![SEBRAUC](ui/src/assets/logo_border.svg)

2
go.mod
View file

@ -3,9 +3,7 @@ module code.thetadev.de/TSGRain/SEBRAUC
go 1.16 go 1.16
require ( require (
code.thetadev.de/ThetaDev/gotry v0.3.2
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/godbus/dbus/v5 v5.0.6
github.com/gofiber/fiber/v2 v2.21.0 github.com/gofiber/fiber/v2 v2.21.0
github.com/gofiber/websocket/v2 v2.0.12 github.com/gofiber/websocket/v2 v2.0.12
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0

7
go.sum
View file

@ -1,15 +1,10 @@
code.thetadev.de/ThetaDev/gotry v0.3.2 h1:x5JOBszLbCo4FDe9V8ynHsV6EfvALV7wUqnJ/5vtjbw=
code.thetadev.de/ThetaDev/gotry v0.3.2/go.mod h1:lKo6abOTMy5uO25ifG7JsGG3DYZd0XZd0xqa6y41BoU=
github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E= github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fasthttp/websocket v1.4.3-rc.9 h1:CWJH0vONrOatdKXZgkgbFKWllijD9aY50C5KfbSDcWk= github.com/fasthttp/websocket v1.4.3-rc.9 h1:CWJH0vONrOatdKXZgkgbFKWllijD9aY50C5KfbSDcWk=
github.com/fasthttp/websocket v1.4.3-rc.9/go.mod h1:eXL2zqDbexYJxaCw8/PQlm7VcMK6uoGvwbYbTdt4dFo= github.com/fasthttp/websocket v1.4.3-rc.9/go.mod h1:eXL2zqDbexYJxaCw8/PQlm7VcMK6uoGvwbYbTdt4dFo=
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.20.1/go.mod h1:/LdZHMUXZvTTo7gU4+b1hclqCAdoQphNQ9bi9gutPyI= github.com/gofiber/fiber/v2 v2.20.1/go.mod h1:/LdZHMUXZvTTo7gU4+b1hclqCAdoQphNQ9bi9gutPyI=
github.com/gofiber/fiber/v2 v2.21.0 h1:tdRNrgqWqcHWBwE3o51oAleEVsil4Ro02zd2vMEuP4Q= github.com/gofiber/fiber/v2 v2.21.0 h1:tdRNrgqWqcHWBwE3o51oAleEVsil4Ro02zd2vMEuP4Q=
github.com/gofiber/fiber/v2 v2.21.0/go.mod h1:MR1usVH3JHYRyQwMe2eZXRSZHRX38fkV+A7CPB+DlDQ= github.com/gofiber/fiber/v2 v2.21.0/go.mod h1:MR1usVH3JHYRyQwMe2eZXRSZHRX38fkV+A7CPB+DlDQ=
@ -45,8 +40,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -45,6 +45,22 @@ paths:
schema: schema:
$ref: "#/components/schemas/StatusMessage" $ref: "#/components/schemas/StatusMessage"
/reboot:
post:
responses:
"200":
description: "OK"
content:
"application/json":
schema:
$ref: "#/components/schemas/StatusMessage"
default:
description: "Server error"
content:
"application/json":
schema:
$ref: "#/components/schemas/StatusMessage"
components: components:
schemas: schemas:
RaucStatus: RaucStatus:

View file

@ -1,88 +1,88 @@
<!-- See https://github.com/gorilla/websocket/blob/master/examples/chat/home.html --> <!-- See https://github.com/gorilla/websocket/blob/master/examples/chat/home.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Chat Example</title> <title>Chat Example</title>
<script type="text/javascript"> <script type="text/javascript">
window.onload = function () { window.onload = function () {
var conn; var conn
var msg = document.getElementById("msg"); var msg = document.getElementById("msg")
var log = document.getElementById("log"); var log = document.getElementById("log")
function appendLog(item) { function appendLog(item) {
var doScroll = var doScroll =
log.scrollTop > log.scrollHeight - log.clientHeight - 1; log.scrollTop > log.scrollHeight - log.clientHeight - 1
log.appendChild(item); log.appendChild(item)
if (doScroll) { if (doScroll) {
log.scrollTop = log.scrollHeight - log.clientHeight; log.scrollTop = log.scrollHeight - log.clientHeight
} }
} }
if (window["WebSocket"]) { if (window["WebSocket"]) {
conn = new WebSocket("ws://" + document.location.host + "/api/ws"); conn = new WebSocket("ws://" + document.location.host + "/api/ws")
conn.onclose = function (evt) { conn.onclose = function (evt) {
var item = document.createElement("div"); var item = document.createElement("div")
item.innerHTML = "<b>Connection closed.</b>"; item.innerHTML = "<b>Connection closed.</b>"
appendLog(item); appendLog(item)
}; }
conn.onmessage = function (evt) { conn.onmessage = function (evt) {
var messages = evt.data.split("\n"); var messages = evt.data.split("\n")
for (var i = 0; i < messages.length; i++) { for (var i = 0; i < messages.length; i++) {
var item = document.createElement("div"); var item = document.createElement("div")
item.innerText = messages[i]; item.innerText = messages[i]
appendLog(item); appendLog(item)
} }
}; }
} else { } else {
var item = document.createElement("div"); var item = document.createElement("div")
item.innerHTML = "<b>Your browser does not support WebSockets.</b>"; item.innerHTML = "<b>Your browser does not support WebSockets.</b>"
appendLog(item); appendLog(item)
} }
}; }
</script> </script>
<style type="text/css"> <style type="text/css">
html { html {
overflow: hidden; overflow: hidden;
} }
body { body {
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
margin: 0; margin: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: gray; background: gray;
} }
#log { #log {
background: white; background: white;
margin: 0; margin: 0;
padding: 0.5em 0.5em 0.5em 0.5em; padding: 0.5em 0.5em 0.5em 0.5em;
position: absolute; position: absolute;
top: 4em; top: 4em;
left: 0.5em; left: 0.5em;
right: 0.5em; right: 0.5em;
bottom: 3em; bottom: 3em;
overflow: auto; overflow: auto;
} }
#form { #form {
padding: 0 0.5em 0 0.5em; padding: 0 0.5em 0 0.5em;
margin: 0; margin: 0;
position: absolute; position: absolute;
bottom: 1em; bottom: 1em;
left: 0px; left: 0px;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="log"></div> <div id="log"></div>
<form method="POST" enctype="multipart/form-data"> <form method="POST" enctype="multipart/form-data">
<input type="file" name="updateFile" id="file" /> <input type="file" name="updateFile" id="file" />
<input type="submit" name="submit" /> <input type="submit" name="submit" />
</form> </form>
</body> </body>
</html> </html>

View file

@ -3,8 +3,6 @@ package fixtures
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"code.thetadev.de/ThetaDev/gotry/try"
) )
func doesFileExist(filepath string) bool { func doesFileExist(filepath string) bool {
@ -13,7 +11,10 @@ func doesFileExist(filepath string) bool {
} }
func getProjectRoot() string { func getProjectRoot() string {
p := try.String(os.Getwd()) p, err := os.Getwd()
if err != nil {
panic(err)
}
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
if doesFileExist(filepath.Join(p, "go.mod")) { if doesFileExist(filepath.Join(p, "go.mod")) {
@ -27,7 +28,10 @@ func getProjectRoot() string {
func CdProjectRoot() { func CdProjectRoot() {
root := getProjectRoot() root := getProjectRoot()
try.Check(os.Chdir(root)) err := os.Chdir(root)
if err != nil {
panic(err)
}
} }
func GetTestfilesDir() string { func GetTestfilesDir() string {

View file

@ -5,7 +5,6 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"code.thetadev.de/ThetaDev/gotry/try"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -17,7 +16,10 @@ func TestGetProjectRoot(t *testing.T) {
t.Run("subdir", func(t *testing.T) { t.Run("subdir", func(t *testing.T) {
root1 := getProjectRoot() root1 := getProjectRoot()
try.Check(os.Chdir(filepath.Join(root1, "src/rauc"))) err := os.Chdir(filepath.Join(root1, "src/rauc"))
if err != nil {
panic(err)
}
root := getProjectRoot() root := getProjectRoot()
assert.True(t, doesFileExist(filepath.Join(root, "go.sum"))) assert.True(t, doesFileExist(filepath.Join(root, "go.sum")))
@ -26,7 +28,10 @@ func TestGetProjectRoot(t *testing.T) {
func TestCdProjectRoot(t *testing.T) { func TestCdProjectRoot(t *testing.T) {
CdProjectRoot() CdProjectRoot()
try.Check(os.Chdir("src/rauc")) err := os.Chdir("src/rauc")
if err != nil {
panic(err)
}
CdProjectRoot() CdProjectRoot()
assert.True(t, doesFileExist("go.sum")) assert.True(t, doesFileExist("go.sum"))
} }

View file

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"regexp" "regexp"
"strconv" "strconv"
@ -18,9 +19,9 @@ var (
) )
type Rauc struct { type Rauc struct {
Command string command string
Args []string args []string
Broadcast chan string broadcast chan string
status RaucStatus status RaucStatus
runningMtx sync.Mutex runningMtx sync.Mutex
} }
@ -33,9 +34,23 @@ type RaucStatus struct {
Log string `json:"log"` Log string `json:"log"`
} }
func (r *Rauc) completed() { func NewRauc(command string, args []string, broadcast chan string) *Rauc {
r := &Rauc{
command: command,
args: args,
broadcast: broadcast,
}
r.broadcast <- r.GetStatusJson()
return r
}
func (r *Rauc) completed(updateFile string) {
r.status.Installing = false r.status.Installing = false
r.Broadcast <- r.GetStatusJson() r.broadcast <- r.GetStatusJson()
_ = os.Remove(updateFile)
} }
func (r *Rauc) RunRauc(updateFile string) error { func (r *Rauc) RunRauc(updateFile string) error {
@ -58,10 +73,10 @@ func (r *Rauc) RunRauc(updateFile string) error {
r.status = RaucStatus{ r.status = RaucStatus{
Installing: true, Installing: true,
} }
r.Broadcast <- r.GetStatusJson() r.broadcast <- r.GetStatusJson()
//nolint:gosec //nolint:gosec
cmd := exec.Command(r.Command, append(r.Args, updateFile)...) cmd := exec.Command(r.command, append(r.args, updateFile)...)
readPipe, _ := cmd.StdoutPipe() readPipe, _ := cmd.StdoutPipe()
cmd.Stderr = cmd.Stdout cmd.Stderr = cmd.Stdout
@ -91,14 +106,14 @@ func (r *Rauc) RunRauc(updateFile string) error {
} }
if hasUpdate { if hasUpdate {
r.Broadcast <- r.GetStatusJson() r.broadcast <- r.GetStatusJson()
} }
} }
}() }()
err := cmd.Start() err := cmd.Start()
if err != nil { if err != nil {
r.completed() r.completed(updateFile)
return err return err
} }
@ -107,7 +122,7 @@ func (r *Rauc) RunRauc(updateFile string) error {
if err != nil { if err != nil {
fmt.Printf("RAUC failed with %s\n", err) fmt.Printf("RAUC failed with %s\n", err)
} }
r.completed() r.completed(updateFile)
}() }()
return nil return nil

View file

@ -25,7 +25,7 @@ func NewHub() *MessageHub {
return &MessageHub{ return &MessageHub{
clients: make(map[*websocket.Conn]hubClient), clients: make(map[*websocket.Conn]hubClient),
register: make(chan *websocket.Conn), register: make(chan *websocket.Conn),
Broadcast: make(chan string), Broadcast: make(chan string, 5),
unregister: make(chan *websocket.Conn), unregister: make(chan *websocket.Conn),
} }
} }

View file

@ -2,13 +2,17 @@ package server
import ( import (
"errors" "errors"
"fmt"
"net/http" "net/http"
"strings" "strings"
"time"
"code.thetadev.de/TSGRain/SEBRAUC/src/assets"
"code.thetadev.de/TSGRain/SEBRAUC/src/rauc" "code.thetadev.de/TSGRain/SEBRAUC/src/rauc"
"code.thetadev.de/TSGRain/SEBRAUC/src/util" "code.thetadev.de/TSGRain/SEBRAUC/src/util"
"code.thetadev.de/TSGRain/SEBRAUC/ui"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/filesystem" "github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/websocket/v2" "github.com/gofiber/websocket/v2"
@ -18,6 +22,8 @@ type SEBRAUCServer struct {
address string address string
raucUpdater *rauc.Rauc raucUpdater *rauc.Rauc
hub *MessageHub hub *MessageHub
tmpdir string
currentId int
} }
type statusMessage struct { type statusMessage struct {
@ -28,31 +34,43 @@ type statusMessage struct {
func NewServer(address string) *SEBRAUCServer { func NewServer(address string) *SEBRAUCServer {
hub := NewHub() hub := NewHub()
raucUpdater := &rauc.Rauc{ raucUpdater := rauc.NewRauc("go", []string{
Command: "go", "run",
Args: []string{ "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock",
"run", }, hub.Broadcast)
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock",
}, tmpdir, err := util.NewTmpdir()
Broadcast: hub.Broadcast, if err != nil {
panic(err)
} }
return &SEBRAUCServer{ return &SEBRAUCServer{
address: address, address: address,
raucUpdater: raucUpdater, raucUpdater: raucUpdater,
hub: hub, hub: hub,
tmpdir: tmpdir,
} }
} }
func (srv *SEBRAUCServer) Run() error { func (srv *SEBRAUCServer) Run() error {
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
AppName: "SEBRAUC", AppName: "SEBRAUC",
BodyLimit: 1024 * 1024 * 1024, BodyLimit: 1024 * 1024 * 1024,
ErrorHandler: errorHandler, ErrorHandler: errorHandler,
DisableStartupMessage: true,
}) })
app.Use(logger.New()) app.Use(logger.New())
app.Use(compress.New(compress.Config{
Next: func(c *fiber.Ctx) bool {
return strings.HasPrefix(c.Path(), "/api")
},
}))
// just for testing
app.Use("/api", cors.New())
app.Use("/api/ws", func(c *fiber.Ctx) error { app.Use("/api/ws", func(c *fiber.Ctx) error {
// IsWebSocketUpgrade returns true if the client // IsWebSocketUpgrade returns true if the client
// requested upgrade to the WebSocket protocol. // requested upgrade to the WebSocket protocol.
@ -64,15 +82,16 @@ func (srv *SEBRAUCServer) Run() error {
}) })
app.Use("/", filesystem.New(filesystem.Config{ app.Use("/", filesystem.New(filesystem.Config{
Root: http.FS(assets.Assets), Root: http.FS(ui.Assets),
PathPrefix: "files", PathPrefix: ui.AssetsDir,
MaxAge: 7200,
})) }))
// ROUTES // ROUTES
app.Get("/api/ws", websocket.New(srv.hub.Handler)) app.Get("/api/ws", websocket.New(srv.hub.Handler))
app.Get("/api/test", srv.controllerTest)
app.Post("/api/update", srv.controllerUpdate) app.Post("/api/update", srv.controllerUpdate)
app.Get("/api/status", srv.controllerStatus) app.Get("/api/status", srv.controllerStatus)
app.Post("/api/reboot", srv.controllerReboot)
// Start messaging hub // Start messaging hub
go srv.hub.Run() go srv.hub.Run()
@ -85,12 +104,16 @@ func (srv *SEBRAUCServer) controllerUpdate(c *fiber.Ctx) error {
if err != nil { if err != nil {
return err return err
} }
err = c.SaveFile(file, "./update.raucb")
srv.currentId++
updateFile := fmt.Sprintf("%s/update_%d.raucb", srv.tmpdir, srv.currentId)
err = c.SaveFile(file, updateFile)
if err != nil { if err != nil {
return err return err
} }
err = srv.raucUpdater.RunRauc("./update.raucb") err = srv.raucUpdater.RunRauc(updateFile)
if err == nil { if err == nil {
writeStatus(c, true, "Update started") writeStatus(c, true, "Update started")
} else if errors.Is(err, util.ErrAlreadyRunning) { } else if errors.Is(err, util.ErrAlreadyRunning) {
@ -107,15 +130,10 @@ func (srv *SEBRAUCServer) controllerStatus(c *fiber.Ctx) error {
return nil return nil
} }
func (srv *SEBRAUCServer) controllerTest(c *fiber.Ctx) error { func (srv *SEBRAUCServer) controllerReboot(c *fiber.Ctx) error {
err := srv.raucUpdater.RunRauc("./update.raucb") go util.Reboot(5 * time.Second)
if err == nil {
writeStatus(c, true, "Update started") writeStatus(c, true, "System is rebooting")
} else if errors.Is(err, util.ErrAlreadyRunning) {
return fiber.NewError(fiber.StatusConflict, "already running")
} else {
return err
}
return nil return nil
} }
@ -123,7 +141,6 @@ func errorHandler(c *fiber.Ctx, err error) error {
// API error handling // API error handling
if strings.HasPrefix(c.Path(), "/api") { if strings.HasPrefix(c.Path(), "/api") {
writeStatus(c, false, err.Error()) writeStatus(c, false, err.Error())
return nil
} }
return err return err
} }

View file

@ -1,8 +1,72 @@
package util package util
import "os" import (
"crypto/rand"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
const tmpdirPrefix = "sebrauc"
func DoesFileExist(filepath string) bool { func DoesFileExist(filepath string) bool {
_, err := os.Stat(filepath) _, err := os.Stat(filepath)
return !os.IsNotExist(err) return !os.IsNotExist(err)
} }
func CreateDirIfNotExists(dirpath string) error {
if _, err := os.Stat(dirpath); os.IsNotExist(err) {
createErr := os.MkdirAll(dirpath, 0o777)
if createErr != nil {
return createErr
}
}
return nil
}
func NewTmpdir() (tmpdir string, err error) {
for {
bts := make([]byte, 16)
_, err = rand.Read(bts)
if err != nil {
return "", err
}
tmpdir = filepath.Join(os.TempDir(), fmt.Sprintf("%s_%x", tmpdirPrefix, bts))
if !DoesFileExist(tmpdir) {
break
}
}
err = CreateDirIfNotExists(tmpdir)
return
}
func PurgeTmpdirs() (count int) {
dirs, _ := os.ReadDir(os.TempDir())
for _, de := range dirs {
if !de.IsDir() {
continue
}
if strings.HasPrefix(de.Name(), tmpdirPrefix+"_") {
err := os.RemoveAll(filepath.Join(os.TempDir(), de.Name()))
if err == nil {
count++
}
}
}
return
}
func Reboot(t time.Duration) {
time.Sleep(t)
/*
cmd := exec.Command("shutdown", "-r", "0")
_ = cmd.Run()
*/
fmt.Println("The system would reboot")
}

View file

@ -1,9 +1,12 @@
package util package util
import ( import (
"os"
"path/filepath"
"testing" "testing"
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures" "code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
"github.com/stretchr/testify/assert"
) )
func TestDoesFileExist(t *testing.T) { func TestDoesFileExist(t *testing.T) {
@ -36,3 +39,32 @@ func TestDoesFileExist(t *testing.T) {
}) })
} }
} }
func TestTmpdir(t *testing.T) {
PurgeTmpdirs()
td, err := NewTmpdir()
if err != nil {
panic(err)
}
tfile := filepath.Join(td, "test.txt")
f, err := os.Create(tfile)
if err != nil {
panic(err)
}
_, err = f.WriteString("Hello")
if err != nil {
panic(err)
}
err = f.Close()
if err != nil {
panic(err)
}
assert.FileExists(t, tfile)
assert.Equal(t, 1, PurgeTmpdirs())
assert.NoFileExists(t, tfile)
}

1
ui/.env.development Normal file
View file

@ -0,0 +1 @@
VITE_API_HOST=127.0.0.1:8080

5
ui/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
node_modules
.DS_Store
dist
dist-ssr
*.local

13
ui/index.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SEBRAUC</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

23
ui/package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "ui",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"serve": "vite preview"
},
"dependencies": {
"@mdi/js": "^6.5.95",
"axios": "^0.24.0",
"byte-size": "^8.1.0",
"preact": "^10.5.15"
},
"devDependencies": {
"@preact/preset-vite": "^2.1.5",
"@types/byte-size": "^8.1.0",
"prettier": "^2.4.1",
"sass": "^1.43.4",
"typescript": "^4.5.2",
"vite": "^2.6.14"
}
}

1222
ui/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="13mm" height="13mm" version="1.1" viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-17.612 -12.829)"><text transform="scale(.26458)" fill="#000000" font-family="sans-serif" font-size="40px" style="line-height:1.25;shape-inside:url(#rect23180);white-space:pre" xml:space="preserve"/><g transform="translate(-.003465 .81595)"><path transform="matrix(.27632 0 0 .27632 9.3797 5.4838)" d="m44.365 23.762 33.173 57.457-66.346-2e-6z" fill="#ff0039"/><g transform="translate(.2726 .27517)" fill="#fff" aria-label="SR"><path d="m19.954 22.779q-0.5715 0-1.0477-0.127-0.46567-0.127-0.79375-0.33866v-1.4182q0.46566 0.26458 0.92075 0.41275 0.45508 0.14817 0.93133 0.14817 0.52916 0 0.87841-0.20108 0.34925-0.20108 0.34925-0.62442 0-0.43392-0.254-0.67733-0.254-0.254-0.65616-0.55033-0.3175-0.23283-0.61383-0.51858-0.29633-0.28575-0.48683-0.66675-0.1905-0.39158-0.1905-0.91016 0-0.67733 0.3175-1.1853 0.3175-0.51858 0.89958-0.81491 0.59266-0.29633 1.3758-0.29633 0.59266 0 1.0901 0.127 0.49742 0.127 0.96308 0.381l-0.56092 1.2065q-0.80433-0.381-1.4922-0.381-0.42333 0-0.6985 0.23283-0.27517 0.22225-0.27517 0.61383 0 0.40216 0.24342 0.64558 0.24342 0.23283 0.68791 0.52916 0.62442 0.41275 0.93133 0.91016 0.3175 0.49742 0.3175 1.1642 0 0.79375-0.39158 1.3123-0.381 0.51858-1.0266 0.77258-0.635 0.254-1.4182 0.254z"/><path d="m23.902 22.755 1.5981-7.5565h1.7145q1.1536 0 1.7568 0.48683t0.60325 1.4711q0 0.84666-0.4445 1.4393-0.43392 0.59266-1.3652 0.86783l1.27 3.2914h-1.2382l-1.0689-3.0586h-1.0583l-0.64558 3.0586zm2.667-4.0111q0.94191 0 1.397-0.41275 0.45508-0.41275 0.45508-1.1007 0-0.58208-0.32808-0.81491-0.32808-0.24342-0.98425-0.24342h-0.68791l-0.55033 2.5717z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

3
ui/src/assets/logo.svg Normal file
View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="100mm" height="25mm" version="1.1" viewBox="0 0 100 25" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-16.98 -12.577)"><text transform="scale(.26458)" fill="#000000" font-family="sans-serif" font-size="40px" style="line-height:1.25;shape-inside:url(#rect23180);white-space:pre" xml:space="preserve"/><path transform="matrix(.34507 0 0 .34507 17.422 6.9892)" d="m44.365 23.762 33.173 57.457-66.346-2e-6z" fill="#ff0039"/><g transform="matrix(2.2124 0 0 2.2124 -21.795 -15.418)" fill="#fff" aria-label="SEBRAUC"><path d="m19.954 22.897q-0.5715 0-1.0477-0.127-0.46567-0.127-0.79375-0.33866v-1.4182q0.46566 0.26458 0.92075 0.41275 0.45508 0.14817 0.93133 0.14817 0.52916 0 0.87841-0.20108 0.34925-0.20108 0.34925-0.62442 0-0.43392-0.254-0.67733-0.254-0.254-0.65616-0.55033-0.3175-0.23283-0.61383-0.51858-0.29633-0.28575-0.48683-0.66675-0.1905-0.39158-0.1905-0.91016 0-0.67733 0.3175-1.1853 0.3175-0.51858 0.89958-0.81491 0.59266-0.29633 1.3758-0.29633 0.59266 0 1.0901 0.127 0.49742 0.127 0.96308 0.381l-0.56092 1.2065q-0.80433-0.381-1.4922-0.381-0.42333 0-0.6985 0.23283-0.27517 0.22225-0.27517 0.61383 0 0.40216 0.24342 0.64558 0.24342 0.23283 0.68791 0.52916 0.62442 0.41275 0.93133 0.91016 0.3175 0.49742 0.3175 1.1642 0 0.79375-0.39158 1.3123-0.381 0.51858-1.0266 0.77258-0.635 0.254-1.4182 0.254z"/><path d="m23.785 22.792 1.5981-7.5565h4.2227l-0.27517 1.3123h-2.6458l-0.34925 1.6616h2.4659l-0.28575 1.3123h-2.4659l-0.41275 1.9473h2.6458l-0.27516 1.3229z"/><path d="m29.521 22.792 1.5981-7.5565h2.2013q1.0795 0 1.7357 0.39158 0.65616 0.381 0.65616 1.2912 0 0.77258-0.43392 1.27-0.43392 0.49742-1.2171 0.66675v0.04233q0.51858 0.13758 0.8255 0.508 0.30692 0.35983 0.30692 0.92075 0 0.85725-0.39158 1.4076-0.39158 0.53975-1.0689 0.80433-0.66675 0.254-1.5134 0.254zm3.3126-4.5614q0.56092 0 0.89958-0.22225 0.34925-0.23283 0.34925-0.75142 0-0.70908-0.87841-0.70908h-0.78316l-0.35983 1.6827zm-0.52916 3.2385q0.58208 0 0.93133-0.28575 0.35983-0.29633 0.35983-0.84666 0-0.40217-0.23283-0.61383-0.23283-0.22225-0.70908-0.22225h-0.85725l-0.42333 1.9685z"/><g><path d="m36.199 22.792 1.5981-7.5565h1.7145q1.1536 0 1.7568 0.48683t0.60325 1.4711q0 0.84666-0.4445 1.4393-0.43392 0.59266-1.3652 0.86783l1.27 3.2914h-1.2382l-1.0689-3.0586h-1.0583l-0.64558 3.0586zm2.667-4.0111q0.94191 0 1.397-0.41275t0.45508-1.1007q0-0.58208-0.32808-0.81491-0.32808-0.24342-0.98425-0.24342h-0.68792l-0.55033 2.5717z"/><path d="m41.853 22.792 4.064-7.5565h1.27l0.87841 7.5565h-1.1112l-0.23283-2.1907h-2.5188l-1.1324 2.1907zm2.8575-3.1856h1.9156l-0.15875-1.6933q-0.03175-0.37042-0.0635-0.80433t-0.04233-0.79375h-0.02117q-0.15875 0.37042-0.34925 0.77258-0.1905 0.40216-0.41275 0.83608z"/><path d="m51.302 22.897q-1.1853 0-1.778-0.55033-0.58208-0.56092-0.58208-1.5557 0-0.17992 0.02117-0.42333 0.03175-0.24342 0.09525-0.508l0.98425-4.6249h1.1218l-0.98425 4.6567q-0.05292 0.22225-0.08467 0.46566-0.02117 0.23283-0.02117 0.39158 0 0.55033 0.32808 0.85725 0.32808 0.30692 0.97366 0.30692 0.86783 0 1.2912-0.45508 0.43392-0.46566 0.62441-1.3652l1.0372-4.8577h1.1324l-1.0583 4.953q-0.16933 0.8255-0.53975 1.4393-0.37042 0.60325-0.99483 0.94191-0.62441 0.32808-1.5663 0.32808z"/><path d="m58.784 22.897q-1.3017 0-2.0108-0.78316t-0.70908-2.1272q0-0.67733 0.15875-1.3652 0.15875-0.6985 0.47625-1.3229 0.32808-0.62442 0.80433-1.1112 0.48683-0.49742 1.143-0.77258 0.66675-0.28575 1.4922-0.28575 0.59266 0 1.0583 0.11642 0.47625 0.10583 0.89958 0.33867l-0.45508 0.93133q-0.30692-0.16933-0.66675-0.28575-0.35983-0.11642-0.83608-0.11642-0.74083 0-1.2912 0.33867-0.53975 0.33866-0.89958 0.91016-0.34925 0.56092-0.52916 1.2382-0.16933 0.67733-0.16933 1.3441 0 0.93133 0.4445 1.4499 0.45508 0.51858 1.3017 0.51858 0.43392 0 0.85725-0.09525t0.83608-0.24342v0.98425q-0.381 0.13758-0.83608 0.23283-0.45508 0.10583-1.0689 0.10583z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="100mm" height="25mm" version="1.1" viewBox="0 0 100 25" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-16.98 -12.577)"><text transform="scale(.26458)" fill="#000000" font-family="sans-serif" font-size="40px" style="line-height:1.25;shape-inside:url(#rect23180);white-space:pre" xml:space="preserve"/><path transform="matrix(.34507 0 0 .34507 17.422 6.9892)" d="m44.365 23.762 33.173 57.457-66.346-2e-6z" fill="#ff0039" stroke="#000" stroke-width="2.3184"/><g transform="matrix(2.2124 0 0 2.2124 -21.795 -15.418)" fill="#fff" stroke="#000" stroke-width=".36159" aria-label="SEBRAUC"><g stroke-width=".36159"><path d="m19.954 22.897q-0.5715 0-1.0477-0.127-0.46567-0.127-0.79375-0.33866v-1.4182q0.46566 0.26458 0.92075 0.41275 0.45508 0.14817 0.93133 0.14817 0.52916 0 0.87841-0.20108 0.34925-0.20108 0.34925-0.62442 0-0.43392-0.254-0.67733-0.254-0.254-0.65616-0.55033-0.3175-0.23283-0.61383-0.51858-0.29633-0.28575-0.48683-0.66675-0.1905-0.39158-0.1905-0.91016 0-0.67733 0.3175-1.1853 0.3175-0.51858 0.89958-0.81491 0.59266-0.29633 1.3758-0.29633 0.59266 0 1.0901 0.127 0.49742 0.127 0.96308 0.381l-0.56092 1.2065q-0.80433-0.381-1.4922-0.381-0.42333 0-0.6985 0.23283-0.27517 0.22225-0.27517 0.61383 0 0.40216 0.24342 0.64558 0.24342 0.23283 0.68791 0.52916 0.62442 0.41275 0.93133 0.91016 0.3175 0.49742 0.3175 1.1642 0 0.79375-0.39158 1.3123-0.381 0.51858-1.0266 0.77258-0.635 0.254-1.4182 0.254z"/><path d="m23.785 22.792 1.5981-7.5565h4.2227l-0.27517 1.3123h-2.6458l-0.34925 1.6616h2.4659l-0.28575 1.3123h-2.4659l-0.41275 1.9473h2.6458l-0.27516 1.3229z"/><path d="m29.521 22.792 1.5981-7.5565h2.2013q1.0795 0 1.7357 0.39158 0.65616 0.381 0.65616 1.2912 0 0.77258-0.43392 1.27-0.43392 0.49742-1.2171 0.66675v0.04233q0.51858 0.13758 0.8255 0.508 0.30692 0.35983 0.30692 0.92075 0 0.85725-0.39158 1.4076-0.39158 0.53975-1.0689 0.80433-0.66675 0.254-1.5134 0.254zm3.3126-4.5614q0.56092 0 0.89958-0.22225 0.34925-0.23283 0.34925-0.75142 0-0.70908-0.87841-0.70908h-0.78316l-0.35983 1.6827zm-0.52916 3.2385q0.58208 0 0.93133-0.28575 0.35983-0.29633 0.35983-0.84666 0-0.40217-0.23283-0.61383-0.23283-0.22225-0.70908-0.22225h-0.85725l-0.42333 1.9685z"/></g><g><path d="m36.199 22.792 1.5981-7.5565h1.7145q1.1536 0 1.7568 0.48683t0.60325 1.4711q0 0.84666-0.4445 1.4393-0.43392 0.59266-1.3652 0.86783l1.27 3.2914h-1.2382l-1.0689-3.0586h-1.0583l-0.64558 3.0586zm2.667-4.0111q0.94191 0 1.397-0.41275t0.45508-1.1007q0-0.58208-0.32808-0.81491-0.32808-0.24342-0.98425-0.24342h-0.68792l-0.55033 2.5717z" stroke-width=".27119"/><path d="m41.853 22.792 4.064-7.5565h1.27l0.87841 7.5565h-1.1112l-0.23283-2.1907h-2.5188l-1.1324 2.1907zm2.8575-3.1856h1.9156l-0.15875-1.6933q-0.03175-0.37042-0.0635-0.80433t-0.04233-0.79375h-0.02117q-0.15875 0.37042-0.34925 0.77258-0.1905 0.40216-0.41275 0.83608z" stroke-width=".27119"/><path d="m51.302 22.897q-1.1853 0-1.778-0.55033-0.58208-0.56092-0.58208-1.5557 0-0.17992 0.02117-0.42333 0.03175-0.24342 0.09525-0.508l0.98425-4.6249h1.1218l-0.98425 4.6567q-0.05292 0.22225-0.08467 0.46566-0.02117 0.23283-0.02117 0.39158 0 0.55033 0.32808 0.85725 0.32808 0.30692 0.97366 0.30692 0.86783 0 1.2912-0.45508 0.43392-0.46566 0.62441-1.3652l1.0372-4.8577h1.1324l-1.0583 4.953q-0.16933 0.8255-0.53975 1.4393-0.37042 0.60325-0.99483 0.94191-0.62441 0.32808-1.5663 0.32808z" stroke-width=".27119"/><path d="m58.784 22.897q-1.3017 0-2.0108-0.78316t-0.70908-2.1272q0-0.67733 0.15875-1.3652 0.15875-0.6985 0.47625-1.3229 0.32808-0.62442 0.80433-1.1112 0.48683-0.49742 1.143-0.77258 0.66675-0.28575 1.4922-0.28575 0.59266 0 1.0583 0.11642 0.47625 0.10583 0.89958 0.33867l-0.45508 0.93133q-0.30692-0.16933-0.66675-0.28575-0.35983-0.11642-0.83608-0.11642-0.74083 0-1.2912 0.33867-0.53975 0.33866-0.89958 0.91016-0.34925 0.56092-0.52916 1.2382-0.16933 0.67733-0.16933 1.3441 0 0.93133 0.4445 1.4499 0.45508 0.51858 1.3017 0.51858 0.43392 0 0.85725-0.09525t0.83608-0.24342v0.98425q-0.381 0.13758-0.83608 0.23283-0.45508 0.10583-1.0689 0.10583z" stroke-width=".27119"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="13mm"
height="13mm"
viewBox="0 0 13 13"
version="1.1"
id="svg5"
sodipodi:docname="logo_square_src.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
fit-margin-top="1"
fit-margin-left="0.5"
fit-margin-right="0.5"
fit-margin-bottom="1"
lock-margins="false"
inkscape:zoom="7.6196617"
inkscape:cx="-0.45933798"
inkscape:cy="16.208069"
inkscape:window-width="2516"
inkscape:window-height="1051"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g24264" />
<defs
id="defs2">
<rect
x="62.302925"
y="21.613937"
width="198.05888"
height="77.345375"
id="rect23180" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-17.612457,-12.829341)">
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text23178"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect23180);fill:#000000;fill-opacity:1;stroke:none" />
<g
id="g24264"
transform="translate(-0.00346504,0.81595258)">
<path
sodipodi:type="star"
style="fill:#ff0039;fill-opacity:1;stroke:none;stroke-width:3.20575;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path30063"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="44.36451"
sodipodi:cy="62.066853"
sodipodi:r1="38.304989"
sodipodi:r2="19.152494"
sodipodi:arg1="-1.5707963"
sodipodi:arg2="-0.52359878"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 44.364511,23.761864 33.173092,57.457484 -66.346187,-2e-6 z"
transform="matrix(0.27469225,0,0,0.27469225,9.4905394,5.4921201)"
inkscape:transform-center-y="-2.6305206" />
<g
aria-label="SR"
id="text4148"
style="font-size:10.5833px;line-height:1.25;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.5;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
transform="translate(0.27260454,0.27516687)">
<path
d="m 19.953951,22.778534 q -0.571498,0 -1.047746,-0.127 -0.465666,-0.126999 -0.793748,-0.338665 v -1.418162 q 0.465665,0.264582 0.920747,0.412748 0.455082,0.148167 0.931331,0.148167 0.529165,0 0.878413,-0.201083 0.349249,-0.201083 0.349249,-0.624415 0,-0.433915 -0.253999,-0.677331 -0.253999,-0.253999 -0.656164,-0.550331 -0.317499,-0.232833 -0.613832,-0.518582 -0.296332,-0.285749 -0.486832,-0.666748 -0.190499,-0.391582 -0.190499,-0.910164 0,-0.677331 0.317499,-1.185329 0.317499,-0.518582 0.89958,-0.814914 0.592665,-0.296333 1.375829,-0.296333 0.592665,0 1.09008,0.127 0.497415,0.126999 0.963081,0.380999 l -0.560915,1.206496 q -0.804331,-0.380999 -1.492246,-0.380999 -0.423332,0 -0.698497,0.232832 -0.275166,0.22225 -0.275166,0.613832 0,0.402165 0.243416,0.645581 0.243416,0.232833 0.687914,0.529165 0.624415,0.412749 0.931331,0.910164 0.317499,0.497415 0.317499,1.164163 0,0.793747 -0.391582,1.312329 -0.380999,0.518582 -1.026581,0.772581 -0.634998,0.253999 -1.418162,0.253999 z"
style="font-style:italic;font-weight:bold;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Bold Italic';fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
id="path55900" />
<path
d="m 23.901529,22.755374 1.598079,-7.556476 h 1.714494 q 1.15358,0 1.756828,0.486832 0.603248,0.486832 0.603248,1.471079 0,0.846664 -0.444499,1.439328 -0.433915,0.592665 -1.365245,0.867831 l 1.269996,3.291406 H 27.796184 L 26.72727,19.6968 h -1.05833 l -0.645581,3.058574 z m 2.666992,-4.01107 q 0.941914,0 1.396995,-0.412749 0.455082,-0.412749 0.455082,-1.100663 0,-0.582082 -0.328082,-0.814914 -0.328082,-0.243416 -0.984247,-0.243416 h -0.687914 l -0.550332,2.571742 z"
style="font-weight:500;-inkscape-font-specification:'Noto Sans Medium Italic';fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1;stroke-width:0.5;stroke-miterlimit:4;stroke-dasharray:none"
id="path55902" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

111
ui/src/assets/logo_src.svg Normal file
View file

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100mm"
height="25mm"
viewBox="0 0 100 25.000001"
version="1.1"
id="svg5"
sodipodi:docname="logo_src.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
fit-margin-top="1"
fit-margin-left="1"
fit-margin-right="1"
fit-margin-bottom="1"
lock-margins="true"
inkscape:zoom="2.9238518"
inkscape:cx="147.23729"
inkscape:cy="11.115475"
inkscape:window-width="2516"
inkscape:window-height="1051"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="text4148" />
<defs
id="defs2">
<rect
x="62.302925"
y="21.613937"
width="198.05888"
height="77.345375"
id="rect23180" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-16.979957,-12.577341)">
<text
xml:space="preserve"
transform="scale(0.26458333)"
id="text23178"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect23180);fill:#000000;fill-opacity:1;stroke:none" />
<path
sodipodi:type="star"
style="fill:#ff0039;fill-opacity:1;stroke:#000000;stroke-width:2.31835097;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path30063"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="44.36451"
sodipodi:cy="62.066853"
sodipodi:r1="38.304989"
sodipodi:r2="19.152494"
sodipodi:arg1="-1.5707963"
sodipodi:arg2="-0.52359878"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 44.364511,23.761864 33.173092,57.457484 -66.346187,-2e-6 z"
transform="matrix(0.34507286,0,0,0.34507286,17.421609,6.9891521)"
inkscape:transform-center-y="-3.3045026" />
<g
aria-label="SEBRAUC"
id="text4148"
style="font-size:10.5833px;line-height:1.25;stroke-width:0.36159238;stroke-miterlimit:4;stroke-dasharray:none"
transform="matrix(2.212436,0,0,2.212436,-21.795233,-15.418287)">
<path
d="m 19.953951,22.897436 q -0.571498,0 -1.047746,-0.127 -0.465666,-0.126999 -0.793748,-0.338665 v -1.418162 q 0.465665,0.264582 0.920747,0.412748 0.455082,0.148167 0.931331,0.148167 0.529165,0 0.878413,-0.201083 0.349249,-0.201083 0.349249,-0.624415 0,-0.433915 -0.253999,-0.677331 -0.253999,-0.253999 -0.656164,-0.550331 -0.317499,-0.232833 -0.613832,-0.518582 -0.296332,-0.285749 -0.486832,-0.666748 -0.190499,-0.391582 -0.190499,-0.910164 0,-0.677331 0.317499,-1.185329 0.317499,-0.518582 0.89958,-0.814914 0.592665,-0.296333 1.375829,-0.296333 0.592665,0 1.09008,0.127 0.497415,0.126999 0.963081,0.380999 l -0.560915,1.206496 q -0.804331,-0.380999 -1.492246,-0.380999 -0.423332,0 -0.698497,0.232832 -0.275166,0.22225 -0.275166,0.613832 0,0.402165 0.243416,0.645581 0.243416,0.232833 0.687914,0.529165 0.624415,0.412749 0.931331,0.910164 0.317499,0.497415 0.317499,1.164163 0,0.793747 -0.391582,1.312329 -0.380999,0.518582 -1.026581,0.772581 -0.634998,0.253999 -1.418162,0.253999 z"
style="font-style:italic;font-weight:bold;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Bold Italic';fill:#ffffff;stroke:#000000;stroke-width:0.36159238;stroke-miterlimit:4;stroke-dasharray:none"
id="path52706" />
<path
d="m 23.785113,22.791603 1.598078,-7.556476 h 4.222737 l -0.275166,1.312329 h -2.645825 l -0.349249,1.661578 h 2.465909 l -0.285749,1.31233 h -2.465909 l -0.412748,1.947327 h 2.645824 l -0.275165,1.322912 z"
style="font-style:italic;font-weight:bold;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Bold Italic';fill:#ffffff;stroke:#000000;stroke-width:0.36159238;stroke-miterlimit:4;stroke-dasharray:none"
id="path52708" />
<path
d="m 29.52126,22.791603 1.598078,-7.556476 h 2.201326 q 1.079497,0 1.735661,0.391582 0.656165,0.380999 0.656165,1.291163 0,0.772581 -0.433915,1.269996 -0.433916,0.497415 -1.21708,0.666748 v 0.04233 q 0.518582,0.137583 0.825497,0.507998 0.306916,0.359832 0.306916,0.920747 0,0.857248 -0.391582,1.407579 -0.391582,0.539748 -1.068913,0.804331 -0.666748,0.253999 -1.513412,0.253999 z m 3.312572,-4.561402 q 0.560915,0 0.899581,-0.222249 0.349249,-0.232833 0.349249,-0.751415 0,-0.709081 -0.878414,-0.709081 h -0.783164 l -0.359833,1.682745 z m -0.529165,3.23849 q 0.582082,0 0.931331,-0.285749 0.359832,-0.296333 0.359832,-0.846664 0,-0.402166 -0.232833,-0.613832 -0.232832,-0.222249 -0.709081,-0.222249 h -0.857247 l -0.423332,1.968494 z"
style="font-style:italic;font-weight:bold;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Bold Italic';fill:#ffffff;stroke:#000000;stroke-width:0.36159238;stroke-miterlimit:4;stroke-dasharray:none"
id="path52710" />
<path
d="m 36.199331,22.791603 1.598078,-7.556476 h 1.714495 q 1.153579,0 1.756827,0.486832 0.603248,0.486832 0.603248,1.471079 0,0.846664 -0.444498,1.439328 -0.433915,0.592665 -1.365246,0.867831 l 1.269996,3.291406 h -1.238246 l -1.068913,-3.058574 h -1.05833 l -0.645581,3.058574 z m 2.666991,-4.01107 q 0.941914,0 1.396996,-0.412749 0.455082,-0.412749 0.455082,-1.100663 0,-0.582082 -0.328082,-0.814914 -0.328083,-0.243416 -0.984247,-0.243416 h -0.687915 l -0.550331,2.571742 z"
style="font-weight:500;-inkscape-font-specification:'Noto Sans Medium Italic';fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-opacity:1;stroke-width:0.27119429;stroke-miterlimit:4;stroke-dasharray:none"
id="path52712" />
<path
d="m 41.852916,22.791603 4.063988,-7.556476 H 47.1869 l 0.878413,7.556476 H 46.954067 L 46.721234,20.60086 h -2.518825 l -1.132413,2.190743 z m 2.857491,-3.185573 h 1.915578 l -0.15875,-1.693328 q -0.03175,-0.370416 -0.0635,-0.804331 -0.03175,-0.433915 -0.04233,-0.793747 h -0.02117 q -0.15875,0.370415 -0.349249,0.772581 -0.1905,0.402165 -0.412749,0.83608 z"
style="font-weight:500;-inkscape-font-specification:'Noto Sans Medium Italic';fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.271194;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path52714" />
<path
d="m 51.301688,22.897436 q -1.18533,0 -1.777994,-0.550331 -0.582082,-0.560915 -0.582082,-1.555746 0,-0.179916 0.02117,-0.423332 0.03175,-0.243415 0.09525,-0.507998 l 0.984247,-4.624902 h 1.12183 l -0.984247,4.656652 q -0.05292,0.222249 -0.08467,0.465665 -0.02117,0.232833 -0.02117,0.391582 0,0.550332 0.328083,0.857248 0.328082,0.306915 0.973663,0.306915 0.867831,0 1.291163,-0.455082 0.433915,-0.465665 0.624414,-1.365245 l 1.037164,-4.857735 h 1.132413 l -1.05833,4.952984 q -0.169333,0.825498 -0.539748,1.439329 -0.370416,0.603248 -0.994831,0.941914 -0.624414,0.328082 -1.566328,0.328082 z"
style="font-weight:500;-inkscape-font-specification:'Noto Sans Medium Italic';fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-opacity:1;stroke-width:0.27119429;stroke-miterlimit:4;stroke-dasharray:none"
id="path52716" />
<path
d="m 58.784078,22.897436 q -1.301746,0 -2.010827,-0.783164 -0.709081,-0.783164 -0.709081,-2.127243 0,-0.677331 0.158749,-1.365246 0.15875,-0.698498 0.476249,-1.322912 0.328082,-0.624415 0.80433,-1.111247 0.486832,-0.497415 1.142997,-0.772581 0.666748,-0.285749 1.492245,-0.285749 0.592665,0 1.05833,0.116416 0.476248,0.105833 0.89958,0.338666 l -0.455081,0.93133 q -0.306916,-0.169332 -0.666748,-0.285749 -0.359832,-0.116416 -0.836081,-0.116416 -0.740831,0 -1.291163,0.338666 -0.539748,0.338665 -0.89958,0.910163 -0.349249,0.560915 -0.529165,1.238246 -0.169333,0.677332 -0.169333,1.344079 0,0.931331 0.444499,1.449913 0.455082,0.518581 1.301746,0.518581 0.433915,0 0.857247,-0.09525 0.423332,-0.09525 0.836081,-0.243416 v 0.984246 q -0.380999,0.137583 -0.836081,0.232833 -0.455082,0.105833 -1.068913,0.105834 z"
style="font-weight:500;-inkscape-font-specification:'Noto Sans Medium Italic';fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-opacity:1;stroke-width:0.27119429;stroke-miterlimit:4;stroke-dasharray:none"
id="path52718" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -0,0 +1,9 @@
.dropzone {
&.highlight {
filter: brightness(0.5);
}
.fileholder {
display: none;
}
}

View file

@ -0,0 +1,119 @@
import {Component, createRef, JSX, ComponentChild, ComponentChildren} from "preact"
import "./Dropzone.scss"
type Props = {
disabled: boolean
clickable: boolean
multiple: boolean
accept?: string
onFilesAdded?: (files: File[]) => void
children?: ComponentChild | ComponentChildren
}
type State = {
highlight: boolean
}
export default class Dropzone extends Component<Props, State> {
static defaultProps = {
disabled: false,
clickable: false,
multiple: false,
}
private fileInputRef = createRef<HTMLInputElement>()
openFileDialog() {
this.fileInputRef.current?.click()
}
reset() {
const input = this.fileInputRef.current
if (input === null) return
input.value = ""
}
private onClick = () => {
if (this.props.disabled || !this.props.clickable) return
this.openFileDialog()
}
private onFilesAdded = (evt: JSX.TargetedEvent) => {
if (this.props.disabled || evt.target === null) return
const input = evt.target as HTMLInputElement
const files = input.files
if (this.props.onFilesAdded) {
const array = this.fileListToArray(files)
this.props.onFilesAdded(array)
}
}
private onDragOver = (evt: DragEvent) => {
evt.preventDefault()
if (this.props.disabled) return
this.setState({highlight: true})
}
private onDragLeave = () => {
this.setState({highlight: false})
}
private onDrop = (evt: DragEvent) => {
evt.preventDefault()
this.setState({highlight: false})
if (this.props.disabled || evt.dataTransfer === null) return
const files = evt.dataTransfer.files
if (!this.props.multiple && files.length > 1) return
if (this.props.onFilesAdded) {
const array = this.fileListToArray(files)
this.props.onFilesAdded(array)
}
}
private fileListToArray(list: FileList | null): File[] {
const array: File[] = []
if (list === null) return array
for (var i = 0; i < list.length; i++) {
array.push(list.item(i)!)
}
return array
}
render() {
return (
<div
class={`dropzone ${this.state.highlight ? "highlight" : ""}`}
onDragOver={this.onDragOver}
onDragLeave={this.onDragLeave}
onDrop={this.onDrop}
onClick={this.onClick}
style={{
cursor:
!this.props.disabled && this.props.clickable
? "pointer"
: "default",
}}
>
<input
ref={this.fileInputRef}
class="fileholder"
type="file"
multiple={this.props.multiple}
accept={this.props.accept}
onInput={this.onFilesAdded}
/>
{this.props.children}
</div>
)
}
}

View file

@ -0,0 +1,30 @@
.icon {
vertical-align: sub;
> svg {
color: inherit;
fill: currentColor;
}
}
.iconButton {
height: 36px;
width: 36px;
display: flex;
align-items: center;
border-radius: 50%;
border: none;
background: #1f85de;
color: #fff;
&:hover {
background-color: #666;
}
&:active {
background-color: #555;
}
}

View file

@ -0,0 +1,29 @@
import {Component} from "preact"
import "./Icon.scss"
type Props = {
icon: string
size: number
color?: string
}
export default class Icon extends Component<Props> {
static defaultProps = {
size: 24,
}
render() {
return (
<span class="icon" style={{color: this.props.color}}>
<svg
xmlns="http://www.w3.org/2000/svg"
style={`height: ${this.props.size}px; width: ${this.props.size}px;`}
viewBox="0 0 24 24"
aria-hidden="true"
>
<path d={this.props.icon} />
</svg>
</span>
)
}
}

View file

@ -0,0 +1,46 @@
.progress-box {
width: 250px;
height: 250px;
position: relative;
svg {
width: 100%;
height: auto;
.progress-path {
transition: stroke-dasharray 0.5s;
}
circle {
transition: fill 0.5s;
}
}
button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
align-items: center;
height: 64px;
width: 64px;
color: white;
background: transparent;
border: none;
cursor: pointer;
border-radius: 50%;
&:hover {
background-color: #666;
}
&:active {
background-color: #555;
}
}
}

View file

@ -0,0 +1,60 @@
import {Component, ComponentChild, ComponentChildren} from "preact"
import "./ProgressCircle.scss"
type Props = {
ready: boolean
progress: number
color: string
children?: ComponentChild | ComponentChildren
}
export default class ProgressCircle extends Component<Props> {
static defaultProps = {
ready: false,
progress: 0,
color: "#FDB900",
}
render() {
const percentage = this.props.ready ? 0 : this.props.progress
const visible = !this.props.ready && this.props.progress > 0
return (
<div class="progress-box">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill={this.props.color} />
{visible ? (
<path
class="progress-path"
stroke-linecap="round"
stroke-width="5"
stroke="#fff"
fill="none"
d="M50 10
a 40 40 0 0 1 0 80
a 40 40 0 0 1 0 -80"
stroke-dasharray={`${percentage * 2.512}, 251.2`}
/>
) : null}
{visible ? (
<text
id="count"
x="50"
y="50"
text-anchor="middle"
dy="7"
font-size="20"
fill="#fff"
>
{percentage}%
</text>
) : null}
</svg>
{visible ? null : this.props.children}
</div>
)
}
}

View file

@ -0,0 +1,29 @@
import {Component} from "preact"
import {mdiTriangleOutline} from "@mdi/js"
import Icon from "../Icon/Icon"
type Props = {
source?: string
message: string
}
export default class Alert extends Component<Props> {
private stripMessage(message: string): string {
return message.replace(/^error:/i, "").trim()
}
render() {
let msg = ""
if (this.props.source !== undefined) msg += `${this.props.source} error: `
msg += this.stripMessage(this.props.message)
return (
<div class="alert">
<span>
<Icon icon={mdiTriangleOutline} color="#FF0039" />
{msg}
</span>
</div>
)
}
}

View file

@ -0,0 +1,51 @@
import {mdiCheckCircleOutline, mdiRestore} from "@mdi/js"
import axios, {AxiosError, AxiosResponse} from "axios"
import {Component} from "preact"
import {apiUrl} from "../../util/apiUrls"
import Icon from "../Icon/Icon"
export default class Reboot extends Component {
private triggerReboot = () => {
const res = confirm("Reboot the system?")
if (!res) return
axios
.post(apiUrl + "/reboot")
.then((response: AxiosResponse) => {
const msg = response.data.msg
if (msg !== undefined) {
alert(msg)
} else {
alert("No response")
}
})
.catch((error: AxiosError) => {
if (error.response) {
const msg = error.response.data.msg
if (msg !== undefined) {
alert("Error: " + msg)
} else {
alert("Error: no response")
}
} else {
alert(String(error))
}
})
}
render() {
return (
<div class="alert">
<span>
<Icon icon={mdiCheckCircleOutline} color="#148420" />
Reboot the system to apply the update
</span>
<button class="iconButton" onClick={this.triggerReboot}>
<Icon icon={mdiRestore} />
</button>
</div>
)
}
}

View file

@ -0,0 +1,44 @@
.uploader {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 0 auto;
max-width: 500px;
width: 90%;
> * {
width: 100%;
}
.card {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 15px 8px;
margin: 8px 0;
text-align: center;
border: 3px solid #fff;
border-radius: 16px;
.top {
font-size: 1.5em;
}
}
.alert {
display: flex;
justify-content: space-between;
margin: 0.6em 0;
border-radius: 16px;
.icon {
margin-right: 0.4em;
}
}
}

View file

@ -0,0 +1,248 @@
import {Component, createRef} from "preact"
import {mdiUpload} from "@mdi/js"
import byteSize from "byte-size"
import Dropzone from "../Dropzone/Dropzone"
import ProgressCircle from "../ProgressCircle/ProgressCircle"
import Icon from "../Icon/Icon"
import "./Updater.scss"
import axios from "axios"
import Alert from "./Alert"
import Reboot from "./Reboot"
import {apiUrl, wsUrl} from "../../util/apiUrls"
class UploadStatus {
uploading = false
total = 0
loaded = 0
lastError = ""
constructor(uploading: boolean, total = 0, loaded = 0, lastError = "") {
this.uploading = uploading
this.total = total
this.loaded = loaded
this.lastError = lastError
}
static fromProgressEvent(progressEvent: {
loaded: number
total: number
}): UploadStatus {
return new UploadStatus(true, progressEvent.total, progressEvent.loaded)
}
}
class RaucStatus {
installing = false
percent = 0
message = ""
last_error = ""
log = ""
}
const byteSizeOptions = {units: "metric"}
type Props = {}
type State = {
uploadStatus: UploadStatus
uploadFilename: string
raucStatus: RaucStatus
wsConnected: boolean
}
export default class Updater extends Component<Props, State> {
private dropzoneRef = createRef<Dropzone>()
private conn: WebSocket | undefined
constructor(props?: Props | undefined, context?: any) {
super(props, context)
this.state = {
uploadStatus: new UploadStatus(false),
uploadFilename: "",
raucStatus: new RaucStatus(),
wsConnected: false,
}
this.connectWebsocket()
}
private buttonClick = () => {
if (!this.acceptUploads()) return
this.dropzoneRef.current?.openFileDialog()
}
private onFilesAdded = (files: File[]) => {
if (files.length === 0) return
const newFile = files[0]
const formData = new FormData()
formData.append("updateFile", newFile)
this.setState({
uploadStatus: new UploadStatus(true, newFile.size, 0),
uploadFilename: newFile.name,
})
axios
.post(apiUrl + "/update", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
onUploadProgress: (progressEvent: {loaded: number; total: number}) => {
this.setState({
uploadStatus: UploadStatus.fromProgressEvent(progressEvent),
})
},
})
.then(() => {
this.resetUpload()
})
.catch((reason: any) => {
this.resetUpload(String(reason))
})
}
private resetUpload = (lastError = "") => {
this.setState({
uploadStatus: new UploadStatus(false, 0, 0, lastError),
})
this.dropzoneRef.current?.reset()
}
private connectWebsocket = () => {
if (window.WebSocket) {
this.conn = new WebSocket(wsUrl)
this.conn.onopen = () => {
this.setState({wsConnected: true})
console.log("WS connected")
}
this.conn.onclose = () => {
this.setState({wsConnected: false})
console.log("WS connection closed")
window.setTimeout(this.connectWebsocket, 3000)
}
this.conn.onmessage = (evt) => {
var messages = evt.data.split("\n")
for (var i = 0; i < messages.length; i++) {
this.setState({
raucStatus: Object.assign(
new RaucStatus(),
JSON.parse(messages[i])
),
})
console.log(this.state.raucStatus)
}
}
} else {
console.log("Your browser does not support WebSockets")
}
}
private acceptUploads(): boolean {
return !this.state.uploadStatus.uploading && !this.state.raucStatus.installing
}
private updateCompleted(): boolean {
return (
!this.state.raucStatus.installing &&
this.state.raucStatus.percent === 100 &&
this.state.raucStatus.last_error === ""
)
}
private uploadPercentage(): number {
if (this.state.uploadStatus.uploading && this.state.uploadStatus.total > 0) {
return Math.round(
(this.state.uploadStatus.loaded / this.state.uploadStatus.total) * 100
)
}
return 0
}
private circleColor(): string {
if (this.state.raucStatus.installing) return "#FF0039"
if (this.state.uploadStatus.uploading) return "#148420"
return "#1f85de"
}
private circlePercentage(): number {
if (this.acceptUploads()) return 0
if (this.state.raucStatus.installing) return this.state.raucStatus.percent
if (this.state.uploadStatus.uploading) return this.uploadPercentage()
return 0
}
render() {
const acceptUploads = this.acceptUploads()
const circleColor = this.circleColor()
const circlePercentage = this.circlePercentage()
const updateCompleted = this.updateCompleted()
let topText = ""
let bottomText = ""
let bottomText2 = ""
if (this.state.uploadStatus.uploading) {
topText = "Uploading"
bottomText = this.state.uploadFilename
bottomText2 = `${byteSize(this.state.uploadStatus.loaded, byteSizeOptions)}
/
${byteSize(this.state.uploadStatus.total, byteSizeOptions)}`
} else if (this.state.raucStatus.installing) {
topText = "Updating firmware"
bottomText = this.state.raucStatus.message
} else {
topText = "Upload firmware package"
}
return (
<div class="uploader">
<div class="card upload">
<div>
<p class="top">{topText}</p>
</div>
<Dropzone
ref={this.dropzoneRef}
onFilesAdded={this.onFilesAdded}
disabled={!acceptUploads}
accept=".raucb"
>
<ProgressCircle
ready={acceptUploads}
progress={circlePercentage}
color={circleColor}
>
<button onClick={this.buttonClick}>
<Icon icon={mdiUpload} size={50} />
</button>
</ProgressCircle>
</Dropzone>
<div>
<p>{bottomText}</p>
<p>{bottomText2}</p>
</div>
</div>
<div>
{this.state.wsConnected ? null : <Alert message="No connection" />}
{this.state.uploadStatus.lastError ? (
<Alert
source="Upload"
message={this.state.uploadStatus.lastError}
/>
) : null}
{this.state.raucStatus.last_error ? (
<Alert
source="Update"
message={this.state.raucStatus.last_error}
/>
) : null}
</div>
{updateCompleted ? <Reboot /> : null}
</div>
)
}
}

14
ui/src/components/app.tsx Normal file
View file

@ -0,0 +1,14 @@
import {Component} from "preact"
import Updater from "./Upload/Updater"
import logo from "../assets/logo.svg"
export default class App extends Component {
render() {
return (
<div>
<img src={logo} height="64" />
<Updater />
</div>
)
}
}

5
ui/src/main.tsx Normal file
View file

@ -0,0 +1,5 @@
import {render} from "preact"
import App from "./components/app"
import "./style/index.scss"
render(<App />, document.getElementById("app")!)

1
ui/src/preact.d.ts vendored Normal file
View file

@ -0,0 +1 @@
import JSX = preact.JSX

26
ui/src/style/index.scss Normal file
View file

@ -0,0 +1,26 @@
html,
body {
height: 100%;
width: 100%;
padding: 0;
margin: 0;
background: #fafafa;
font-family: "Helvetica Neue", arial, sans-serif;
font-weight: 400;
color: #444;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #222;
color: #fff;
}
#app {
padding-top: 100px;
font-size: 1.2em;
text-align: center;
}
p {
margin: 0.3em 0;
}

12
ui/src/util/apiUrls.ts Normal file
View file

@ -0,0 +1,12 @@
let apiHost = document.location.host
const httpProto = document.location.protocol
const wsProto = httpProto === "https:" ? "wss:" : "ws:"
if (import.meta.env.VITE_API_HOST !== undefined) {
apiHost = import.meta.env.VITE_API_HOST as string
}
const apiUrl = `${httpProto}//${apiHost}/api`
const wsUrl = `${wsProto}//${apiHost}/api/ws`
export {apiUrl, wsUrl}

1
ui/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

21
ui/tsconfig.json Normal file
View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}

10
ui/ui.go Normal file
View file

@ -0,0 +1,10 @@
package ui
import (
"embed"
)
const AssetsDir = "dist"
//go:embed dist/**
var Assets embed.FS

7
ui/vite.config.ts Normal file
View file

@ -0,0 +1,7 @@
import {defineConfig} from "vite"
import preact from "@preact/preset-vite"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [preact()],
})