Compare commits

...

3 commits

Author SHA1 Message Date
44001bb7e7 add hostname to sysinfo
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-06 00:59:23 +01:00
7465ef3380 add system info, openapi-generated client 2021-12-06 00:52:14 +01:00
85c0073651 Add sysinfo api endpoint 2021-12-03 00:49:20 +01:00
31 changed files with 1670 additions and 59 deletions

View file

@ -17,6 +17,9 @@ build-server:
build: build-ui build-server build: build-ui build-server
generate-apiclient:
openapi-generator generate -i openapi.yml -g typescript-axios -o ${UI_DIR}/src/sebrauc-client -p "supportsES6=true"
clean: clean:
rm -f build/* rm -f build/*
rm -rf ${UI_DIR}/dist/** rm -rf ${UI_DIR}/dist/**

1
go.sum
View file

@ -19,6 +19,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4 h1:ocK/D6lCgLji37Z2so4xhMl46se1ntReQQCUIU4BWI8= github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4 h1:ocK/D6lCgLji37Z2so4xhMl46se1ntReQQCUIU4BWI8=
github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas= github.com/savsgio/gotils v0.0.0-20210921075833-21a6215cb0e4/go.mod h1:oejLrk1Y/5zOF+c/aHtXqn3TFlzzbAgPWg8zBiAHDas=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View file

@ -118,6 +118,10 @@ components:
SystemInfo: SystemInfo:
type: object type: object
properties: properties:
hostname:
description: Hostname of the system
type: string
example: "raspberrypi3"
os_name: os_name:
description: Name of the os distribution description: Name of the os distribution
type: string type: string
@ -138,21 +142,21 @@ components:
description: Compatible firmware variant description: Compatible firmware variant
type: string type: string
example: "rpi-prod" example: "rpi-prod"
rauc_booted:
description: Currently booted rootfs
type: string
example: "rootfs.0"
rauc_boot_primary:
description: Primary rootfs to boot from
type: string
example: "rootfs.1"
rauc_rootfs: rauc_rootfs:
description: List of RAUC root filesystems description: List of RAUC root filesystems
type: object type: object
additionalProperties: additionalProperties:
$ref: "#/components/schemas/RaucFS" $ref: "#/components/schemas/Rootfs"
required:
- hostname
- os_name
- os_version
- uptime
- rauc_compatible
- rauc_variant
- rauc_rootfs
RaucFS: Rootfs:
type: object type: object
properties: properties:
device: device:
@ -163,11 +167,6 @@ components:
description: Filesystem description: Filesystem
type: string type: string
example: ext4 example: ext4
state:
description: Current state of filesystem
type: string
enum: [active, inactive, booted]
example: booted
mountpoint: mountpoint:
description: Mount path (null when not mounted) description: Mount path (null when not mounted)
type: string type: string
@ -176,6 +175,19 @@ components:
bootable: bootable:
description: "Is the filesystem bootable" description: "Is the filesystem bootable"
type: boolean type: boolean
booted:
description: "Is the filesystem booted"
type: boolean
primary:
description: "Is the filesystem the next boot target"
type: boolean
required:
- device
- type
- mountpoint
- bootable
- booted
- primary
StatusMessage: StatusMessage:
type: object type: object
@ -188,4 +200,5 @@ components:
type: string type: string
example: Update started example: Update started
required: required:
- success
- msg - msg

View file

@ -41,22 +41,34 @@ LastError: Failed to check bundle identifier: Invalid identifier. ` +
idle idle
Installing ` + "/app/demo` failed" Installing ` + "/app/demo` failed"
const statusJson = `{"compatible":"TSGRain","variant":"dev","booted":"A",` +
`"boot_primary":"rootfs.0","slots":[{"rootfs.1":{"class":"rootfs",` +
`"device":"/dev/mmcblk0p3","type":"ext4","bootname":"B","state":"inactive",` +
`"parent":null,"mountpoint":null,"boot_status":"good"}},{"rootfs.0":` +
`{"class":"rootfs","device":"/dev/mmcblk0p2","type":"ext4","bootname":"A",` +
`"state":"booted","parent":null,"mountpoint":"/","boot_status":"good"}}]}`
func printLinesWithDelay(lines string) {
for _, line := range strings.Split(lines, "\n") {
fmt.Println(line)
time.Sleep(500 * time.Millisecond)
}
}
func main() { func main() {
arg := "" arg := ""
if len(os.Args) > 1 { if len(os.Args) > 1 {
arg = os.Args[1] arg = os.Args[1]
} }
var lines string
switch arg { switch arg {
case "fail": case "fail":
lines = outputFailure printLinesWithDelay(outputFailure)
case "install":
printLinesWithDelay(outputSuccess)
case "status":
fmt.Println(statusJson)
default: default:
lines = outputSuccess os.Exit(1)
}
for _, line := range strings.Split(lines, "\n") {
fmt.Println(line)
time.Sleep(500 * time.Millisecond)
} }
} }

View file

@ -0,0 +1,5 @@
ID=tsgrain
NAME="TSGRain distro"
VERSION="0.0.1"
VERSION_ID=0.0.1
PRETTY_NAME="TSGRain distro 0.0.1"

View file

@ -70,7 +70,7 @@ func (r *Rauc) RunRauc(updateFile string) error {
} }
r.broadcast <- r.GetStatusJson() r.broadcast <- r.GetStatusJson()
cmd := util.CommandFromString(fmt.Sprintf("%s %s", util.UpdateCmd, updateFile)) cmd := util.CommandFromString(fmt.Sprintf("%s install %s", util.RaucCmd, updateFile))
readPipe, _ := cmd.StdoutPipe() readPipe, _ := cmd.StdoutPipe()
cmd.Stderr = cmd.Stdout cmd.Stderr = cmd.Stdout

View file

@ -8,6 +8,7 @@ import (
"time" "time"
"code.thetadev.de/TSGRain/SEBRAUC/src/rauc" "code.thetadev.de/TSGRain/SEBRAUC/src/rauc"
"code.thetadev.de/TSGRain/SEBRAUC/src/sysinfo"
"code.thetadev.de/TSGRain/SEBRAUC/src/util" "code.thetadev.de/TSGRain/SEBRAUC/src/util"
"code.thetadev.de/TSGRain/SEBRAUC/ui" "code.thetadev.de/TSGRain/SEBRAUC/ui"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@ -88,6 +89,7 @@ func (srv *SEBRAUCServer) Run() error {
app.Get("/api/ws", websocket.New(srv.hub.Handler)) app.Get("/api/ws", websocket.New(srv.hub.Handler))
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.Get("/api/info", srv.controllerInfo)
app.Post("/api/reboot", srv.controllerReboot) app.Post("/api/reboot", srv.controllerReboot)
// Start messaging hub // Start messaging hub
@ -131,6 +133,17 @@ func (srv *SEBRAUCServer) controllerStatus(c *fiber.Ctx) error {
return nil return nil
} }
func (srv *SEBRAUCServer) controllerInfo(c *fiber.Ctx) error {
info, err := sysinfo.GetSysinfo()
if err != nil {
return err
}
c.Context().SetStatusCode(200)
_ = c.JSON(info)
return nil
}
func (srv *SEBRAUCServer) controllerReboot(c *fiber.Ctx) error { func (srv *SEBRAUCServer) controllerReboot(c *fiber.Ctx) error {
go util.Reboot(5 * time.Second) go util.Reboot(5 * time.Second)

180
src/sysinfo/sysinfo.go Normal file
View file

@ -0,0 +1,180 @@
package sysinfo
import (
"encoding/json"
"os"
"regexp"
"strconv"
"strings"
"code.thetadev.de/TSGRain/SEBRAUC/src/util"
)
type SystemInfo struct {
Hostname string `json:"hostname"`
OsName string `json:"os_name"`
OsVersion string `json:"os_version"`
Uptime int `json:"uptime"`
RaucCompatible string `json:"rauc_compatible"`
RaucVariant string `json:"rauc_variant"`
RaucRootfs map[string]Rootfs `json:"rauc_rootfs"`
}
type Rootfs struct {
Device string `json:"device"`
Type string `json:"type"`
bootname string
Mountpoint *string `json:"mountpoint"`
Bootable bool `json:"bootable"`
Booted bool `json:"booted"`
Primary bool `json:"primary"`
}
type raucInfo struct {
Compatible string `json:"compatible"`
Variant string `json:"variant"`
Booted string `json:"booted"`
BootPrimary string `json:"boot_primary"`
Slots []map[string]raucFS `json:"slots"`
}
type raucFS struct {
Class string `json:"class"`
Device string `json:"device"`
Type string `json:"type"`
Bootname string `json:"bootname"`
State string `json:"state"`
Mountpoint *string `json:"mountpoint"`
BootStatus string `json:"boot_status"`
}
type osRelease struct {
OsName string `json:"os_name"`
OsVersion string `json:"os_version"`
}
var (
rexpOsName = regexp.MustCompile(`(?m)^NAME="(.+)"`)
rexpOsVersion = regexp.MustCompile(`(?m)^VERSION="(.+)"`)
rexpUptime = regexp.MustCompile(`^\d+`)
)
func parseRaucInfo(raucInfoJson []byte) (raucInfo, error) {
res := raucInfo{}
err := json.Unmarshal(raucInfoJson, &res)
return res, err
}
func parseOsRelease(osReleaseFile string) (osRelease, error) {
osReleaseTxt, err := os.ReadFile(osReleaseFile)
if err != nil {
return osRelease{}, err
}
nameMatch := rexpOsName.FindSubmatch(osReleaseTxt)
versionMatch := rexpOsVersion.FindSubmatch(osReleaseTxt)
name := ""
if nameMatch != nil {
name = string(nameMatch[1])
}
version := ""
if versionMatch != nil {
version = string(versionMatch[1])
}
return osRelease{
OsName: name,
OsVersion: version,
}, nil
}
func mapRootfs(rinf raucInfo) map[string]Rootfs {
res := make(map[string]Rootfs)
for _, slot := range rinf.Slots {
for name, fs := range slot {
if fs.Class == "rootfs" {
res[name] = Rootfs{
Device: fs.Device,
Type: fs.Type,
bootname: fs.Bootname,
Mountpoint: fs.Mountpoint,
Bootable: fs.BootStatus == "good",
Booted: fs.State == "booted",
Primary: rinf.BootPrimary == name,
}
}
}
}
return res
}
func getFSNameFromBootname(rfslist map[string]Rootfs, bootname string) string {
for name, rfs := range rfslist {
if rfs.bootname == bootname {
return name
}
}
return "n/a"
}
func mapSysinfo(rinf raucInfo, osr osRelease, uptime int, hostname string) SystemInfo {
rfslist := mapRootfs(rinf)
return SystemInfo{
Hostname: hostname,
OsName: osr.OsName,
OsVersion: osr.OsVersion,
Uptime: uptime,
RaucCompatible: rinf.Compatible,
RaucVariant: rinf.Variant,
RaucRootfs: rfslist,
}
}
func getUptime() (int, error) {
uptimeRaw, err := os.ReadFile("/proc/uptime")
if err != nil {
return 0, err
}
uptimeChars := rexpUptime.Find(uptimeRaw)
return strconv.Atoi(string(uptimeChars))
}
func getHostname() string {
hostname, err := os.ReadFile("/etc/hostname")
if err != nil {
return ""
}
return strings.TrimSpace(string(hostname))
}
func GetSysinfo() (SystemInfo, error) {
cmd := util.CommandFromString(util.RaucCmd + " status")
rinfJson, err := cmd.Output()
if err != nil {
return SystemInfo{}, err
}
rinf, err := parseRaucInfo(rinfJson)
if err != nil {
return SystemInfo{}, err
}
osinf, err := parseOsRelease("/etc/os-release")
if err != nil {
return SystemInfo{}, err
}
uptime, err := getUptime()
if err != nil {
return SystemInfo{}, err
}
hostname := getHostname()
return mapSysinfo(rinf, osinf, uptime, hostname), nil
}

122
src/sysinfo/sysinfo_test.go Normal file
View file

@ -0,0 +1,122 @@
package sysinfo
import (
"path/filepath"
"testing"
"code.thetadev.de/TSGRain/SEBRAUC/src/fixtures"
"github.com/stretchr/testify/assert"
)
const statusJson = `{"compatible":"TSGRain","variant":"dev","booted":"A",` +
`"boot_primary":"rootfs.0","slots":[{"rootfs.1":{"class":"rootfs",` +
`"device":"/dev/mmcblk0p3","type":"ext4","bootname":"B","state":"inactive",` +
`"parent":null,"mountpoint":null,"boot_status":"good"}},{"rootfs.0":` +
`{"class":"rootfs","device":"/dev/mmcblk0p2","type":"ext4","bootname":"A",` +
`"state":"booted","parent":null,"mountpoint":"/","boot_status":"good"}}]}`
var mountRoot = "/"
var expectedRaucInfo = raucInfo{
Compatible: "TSGRain",
Variant: "dev",
Booted: "A",
BootPrimary: "rootfs.0",
Slots: []map[string]raucFS{
{
"rootfs.1": {
Class: "rootfs",
Device: "/dev/mmcblk0p3",
Type: "ext4",
Bootname: "B",
State: "inactive",
Mountpoint: nil,
BootStatus: "good",
},
},
{
"rootfs.0": {
Class: "rootfs",
Device: "/dev/mmcblk0p2",
Type: "ext4",
Bootname: "A",
State: "booted",
Mountpoint: &mountRoot,
BootStatus: "good",
},
},
},
}
var expectedRootfsList = map[string]Rootfs{
"rootfs.0": {
Device: "/dev/mmcblk0p2",
Type: "ext4",
bootname: "A",
Mountpoint: &mountRoot,
Bootable: true,
Booted: true,
Primary: true,
},
"rootfs.1": {
Device: "/dev/mmcblk0p3",
Type: "ext4",
bootname: "B",
Mountpoint: nil,
Bootable: true,
Booted: false,
Primary: false,
},
}
func TestParseRaucInfo(t *testing.T) {
info, err := parseRaucInfo([]byte(statusJson))
if err != nil {
panic(err)
}
assert.Equal(t, expectedRaucInfo, info)
}
func TestParseOsRelease(t *testing.T) {
testfiles := fixtures.GetTestfilesDir()
osReleaseFile := filepath.Join(testfiles, "os-release")
osRel, err := parseOsRelease(osReleaseFile)
if err != nil {
panic(err)
}
expected := osRelease{
OsName: "TSGRain distro",
OsVersion: "0.0.1",
}
assert.Equal(t, expected, osRel)
}
func TestMapRootfsList(t *testing.T) {
rootfsList := mapRootfs(expectedRaucInfo)
assert.Equal(t, expectedRootfsList, rootfsList)
}
func TestGetFSNameFromBootname(t *testing.T) {
rootfsList := mapRootfs(expectedRaucInfo)
assert.Equal(t, "rootfs.0", getFSNameFromBootname(rootfsList, "A"))
assert.Equal(t, "rootfs.1", getFSNameFromBootname(rootfsList, "B"))
assert.Equal(t, "n/a", getFSNameFromBootname(rootfsList, "C"))
}
func TestGetSysinfo(t *testing.T) {
sysinfo, err := GetSysinfo()
if err != nil {
panic(err)
}
assert.Greater(t, sysinfo.Uptime, 0)
assert.Equal(t, "TSGRain", sysinfo.RaucCompatible)
assert.Equal(t, "dev", sysinfo.RaucVariant)
assert.Equal(t, expectedRootfsList, sysinfo.RaucRootfs)
}

View file

@ -5,7 +5,7 @@ package util
const ( const (
RebootCmd = "shutdown -r 0" RebootCmd = "shutdown -r 0"
UpdateCmd = "rauc install" RaucCmd = "rauc"
TestMode = false TestMode = false
) )

View file

@ -5,7 +5,7 @@ package util
const ( const (
RebootCmd = "touch /tmp/sebrauc_reboot_test" RebootCmd = "touch /tmp/sebrauc_reboot_test"
UpdateCmd = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock" RaucCmd = "go run code.thetadev.de/TSGRain/SEBRAUC/src/fixtures/rauc_mock"
TestMode = true TestMode = true
) )

View file

@ -1,6 +1,7 @@
import {Component} from "preact" import {Component} from "preact"
import {mdiTriangleOutline} from "@mdi/js" import {mdiTriangleOutline} from "@mdi/js"
import Icon from "../Icon/Icon" import Icon from "../Icon/Icon"
import colors from "../../util/colors"
type Props = { type Props = {
source?: string source?: string
@ -20,7 +21,7 @@ export default class Alert extends Component<Props> {
return ( return (
<div class="alert"> <div class="alert">
<span> <span>
<Icon icon={mdiTriangleOutline} color="#FF0039" /> <Icon icon={mdiTriangleOutline} color={colors.RED} />
{msg} {msg}
</span> </span>
</div> </div>

View file

@ -1,7 +1,6 @@
import {mdiCheckCircleOutline, mdiRestore} from "@mdi/js" import {mdiCheckCircleOutline, mdiRestore} from "@mdi/js"
import axios, {AxiosError, AxiosResponse} from "axios"
import {Component} from "preact" import {Component} from "preact"
import {apiUrl} from "../../util/apiUrls" import {sebraucApi} from "../../util/apiUrls"
import Icon from "../Icon/Icon" import Icon from "../Icon/Icon"
export default class Reboot extends Component { export default class Reboot extends Component {
@ -9,9 +8,9 @@ export default class Reboot extends Component {
const res = confirm("Reboot the system?") const res = confirm("Reboot the system?")
if (!res) return if (!res) return
axios sebraucApi
.post(apiUrl + "/reboot") .startReboot()
.then((response: AxiosResponse) => { .then((response) => {
const msg = response.data.msg const msg = response.data.msg
if (msg !== undefined) { if (msg !== undefined) {
@ -20,7 +19,7 @@ export default class Reboot extends Component {
alert("No response") alert("No response")
} }
}) })
.catch((error: AxiosError) => { .catch((error) => {
if (error.response) { if (error.response) {
const msg = error.response.data.msg const msg = error.response.data.msg

View file

@ -0,0 +1,159 @@
import {Component} from "preact"
import {SystemInfo} from "../../sebrauc-client"
import {sebraucApi} from "../../util/apiUrls"
import {secondsToString} from "../../util/functions"
import Icon from "../Icon/Icon"
import {
mdiAlphaVCircleOutline,
mdiCheckCircleOutline,
mdiCircleOutline,
mdiClockOutline,
mdiCloseCircleOutline,
mdiMonitor,
mdiPenguin,
mdiTagMultipleOutline,
mdiTagOutline,
} from "@mdi/js"
import colors from "../../util/colors"
type Props = {}
type State = {
sysinfo: SystemInfo
}
export default class SysinfoCard extends Component<Props, State> {
constructor(props?: Props | undefined, context?: any) {
super(props, context)
this.fetchInfo()
}
private fetchInfo = () => {
sebraucApi
.getInfo()
.then((response) => {
if (response.status == 200) {
this.setState({sysinfo: response.data})
} else {
console.log("error fetching info", response.data)
console.log("error fetching info", response.data)
window.setTimeout(this.fetchInfo, 3000)
}
})
.catch((reason) => {
console.log("error fetching info", reason)
window.setTimeout(this.fetchInfo, 3000)
})
}
private renderSysinfo() {
return (
<div>
<div className="card">
<p class="top">System information</p>
<table class="table no-bottom-border">
<tr>
<td>
<Icon icon={mdiMonitor} /> Hostname
</td>
<td>{this.state.sysinfo.hostname}</td>
</tr>
<tr>
<td>
<Icon icon={mdiPenguin} /> Operating system
</td>
<td>{this.state.sysinfo.os_name}</td>
</tr>
<tr>
<td>
<Icon icon={mdiAlphaVCircleOutline} /> OS version
</td>
<td>{this.state.sysinfo.os_version}</td>
</tr>
<tr>
<td>
<Icon icon={mdiClockOutline} /> Uptime
</td>
<td>{secondsToString(this.state.sysinfo.uptime)}</td>
</tr>
<tr>
<td>
<Icon icon={mdiTagOutline} /> Compatible FW
</td>
<td>{this.state.sysinfo.rauc_compatible}</td>
</tr>
<tr>
<td>
<Icon icon={mdiTagMultipleOutline} /> Compatible FW
variant
</td>
<td>{this.state.sysinfo.rauc_variant}</td>
</tr>
</table>
</div>
<div className="card">
<p class="top">Rootfs slots</p>
<div class="table-wrapper">
<table class="table no-bottom-border">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Device</th>
<th>Mountpoint</th>
</tr>
</thead>
<tbody>
{Object.keys(this.state.sysinfo.rauc_rootfs).map(
(k, i) => {
const rfs = this.state.sysinfo.rauc_rootfs[k]
let icon = mdiCircleOutline
let iconColor = colors.BLUE
if (!rfs.bootable) {
icon = mdiCloseCircleOutline
iconColor = colors.RED
} else if (rfs.primary) {
icon = mdiCheckCircleOutline
iconColor = colors.GREEN
}
return (
<tr key={i}>
<td>
<Icon
icon={icon}
color={iconColor}
/>
</td>
<td>{k}</td>
<td>{rfs.device}</td>
<td>{rfs.mountpoint}</td>
</tr>
)
}
)}
</tbody>
</table>
</div>
</div>
</div>
)
}
private renderLoadingAnimation() {
return (
<div className="card">
<p>loading sysinfo...</p>
</div>
)
}
render() {
if (this.state.sysinfo) {
return this.renderSysinfo()
}
return this.renderLoadingAnimation()
}
}

View file

@ -1,11 +1,11 @@
.uploader { .updater-view {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin: 0 auto; margin: 0 auto;
max-width: 500px; max-width: 600px;
width: 90%; width: 90%;
> * { > * {
@ -18,8 +18,8 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 15px 8px; margin-top: 25px;
margin: 8px 0; margin-bottom: 8px;
text-align: center; text-align: center;
@ -29,6 +29,14 @@
.top { .top {
font-size: 1.5em; font-size: 1.5em;
} }
&.pad {
padding: 15px 0;
}
&:first-of-type {
margin-top: 8px;
}
} }
.alert { .alert {
@ -42,3 +50,9 @@
} }
} }
} }
.button-top-right {
position: absolute;
top: 20px;
right: 20px;
}

View file

@ -5,10 +5,10 @@ import Dropzone from "../Dropzone/Dropzone"
import ProgressCircle from "../ProgressCircle/ProgressCircle" import ProgressCircle from "../ProgressCircle/ProgressCircle"
import Icon from "../Icon/Icon" import Icon from "../Icon/Icon"
import "./Updater.scss" import "./Updater.scss"
import axios from "axios"
import Alert from "./Alert" import Alert from "./Alert"
import Reboot from "./Reboot" import Reboot from "./Reboot"
import {apiUrl, wsUrl} from "../../util/apiUrls" import {sebraucApi, wsUrl} from "../../util/apiUrls"
import colors from "../../util/colors"
class UploadStatus { class UploadStatus {
uploading = false uploading = false
@ -50,7 +50,7 @@ type State = {
wsConnected: boolean wsConnected: boolean
} }
export default class Updater extends Component<Props, State> { export default class UpdaterCard extends Component<Props, State> {
private dropzoneRef = createRef<Dropzone>() private dropzoneRef = createRef<Dropzone>()
private conn: WebSocket | undefined private conn: WebSocket | undefined
@ -77,19 +77,13 @@ export default class Updater extends Component<Props, State> {
if (files.length === 0) return if (files.length === 0) return
const newFile = files[0] const newFile = files[0]
const formData = new FormData()
formData.append("updateFile", newFile)
this.setState({ this.setState({
uploadStatus: new UploadStatus(true, newFile.size, 0), uploadStatus: new UploadStatus(true, newFile.size, 0),
uploadFilename: newFile.name, uploadFilename: newFile.name,
}) })
axios sebraucApi
.post(apiUrl + "/update", formData, { .startUpdate(newFile, {
headers: {
"Content-Type": "multipart/form-data",
},
onUploadProgress: (progressEvent: {loaded: number; total: number}) => { onUploadProgress: (progressEvent: {loaded: number; total: number}) => {
this.setState({ this.setState({
uploadStatus: UploadStatus.fromProgressEvent(progressEvent), uploadStatus: UploadStatus.fromProgressEvent(progressEvent),
@ -132,8 +126,6 @@ export default class Updater extends Component<Props, State> {
JSON.parse(messages[i]) JSON.parse(messages[i])
), ),
}) })
console.log(this.state.raucStatus)
} }
} }
} else { } else {
@ -163,9 +155,9 @@ export default class Updater extends Component<Props, State> {
} }
private circleColor(): string { private circleColor(): string {
if (this.state.raucStatus.installing) return "#FF0039" if (this.state.raucStatus.installing) return colors.RED
if (this.state.uploadStatus.uploading) return "#148420" if (this.state.uploadStatus.uploading) return colors.GREEN
return "#1f85de" return colors.BLUE
} }
private circlePercentage(): number { private circlePercentage(): number {
@ -195,12 +187,13 @@ export default class Updater extends Component<Props, State> {
topText = "Updating firmware" topText = "Updating firmware"
bottomText = this.state.raucStatus.message bottomText = this.state.raucStatus.message
} else { } else {
topText = "Upload firmware package" topText = "Firmware update"
bottomText = "Upload *.raucb FW package"
} }
return ( return (
<div class="uploader"> <div>
<div class="card upload"> <div class="card pad">
<div> <div>
<p class="top">{topText}</p> <p class="top">{topText}</p>
</div> </div>

View file

@ -0,0 +1,48 @@
import {mdiInformation, mdiUpload} from "@mdi/js"
import {Component} from "preact"
import Icon from "../Icon/Icon"
import SysinfoCard from "./SysinfoCard"
import UpdaterCard from "./UpdaterCard"
import "./Updater.scss"
type Props = {}
type State = {
flipped: boolean
}
export default class UpdaterView extends Component<Props, State> {
constructor(props?: Props | undefined, context?: any) {
super(props, context)
this.state = {
flipped: false,
}
}
private flipCard = () => {
this.setState({flipped: !this.state.flipped})
}
render() {
return (
<div>
<button
class="iconButton button-top-right"
onClick={this.flipCard}
aria-label={
this.state.flipped
? "Switch to updater"
: "Switch to system info"
}
>
<Icon icon={this.state.flipped ? mdiUpload : mdiInformation} />
</button>
<div className="updater-view">
{!this.state.flipped ? <UpdaterCard /> : <SysinfoCard />}
</div>
</div>
)
}
}

View file

@ -1,5 +1,5 @@
import {Component} from "preact" import {Component} from "preact"
import Updater from "./Updater/Updater" import UpdaterView from "./Updater/UpdaterView"
import logo from "../assets/logo.svg" import logo from "../assets/logo.svg"
import {version} from "../util/version" import {version} from "../util/version"
@ -9,7 +9,7 @@ export default class App extends Component {
<div> <div>
<img src={logo} alt="SEBRAUC" height="64" /> <img src={logo} alt="SEBRAUC" height="64" />
{version} {version}
<Updater /> <UpdaterView />
</div> </div>
) )
} }

View file

@ -0,0 +1,27 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md
/.gitignore
/.npmignore
/git_push.sh

View file

@ -0,0 +1,5 @@
api.ts
base.ts
common.ts
configuration.ts
index.ts

View file

@ -0,0 +1 @@
5.3.0

View file

@ -0,0 +1,539 @@
/* tslint:disable */
/* eslint-disable */
/**
* SEBRAUC
* REST API for the SEBRAUC firmware updater
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import {Configuration} from "./configuration"
import globalAxios, {AxiosPromise, AxiosInstance, AxiosRequestConfig} from "axios"
// Some imports not used depending on template conditions
// @ts-ignore
import {
DUMMY_BASE_URL,
assertParamExists,
setApiKeyToObject,
setBasicAuthToObject,
setBearerAuthToObject,
setOAuthToObject,
setSearchParams,
serializeDataIfNeeded,
toPathString,
createRequestFunction,
} from "./common"
// @ts-ignore
import {
BASE_PATH,
COLLECTION_FORMATS,
RequestArgs,
BaseAPI,
RequiredError,
} from "./base"
/**
*
* @export
* @interface RaucStatus
*/
export interface RaucStatus {
/**
* True if the installer is running
* @type {boolean}
* @memberof RaucStatus
*/
installing: boolean
/**
* Installation progress
* @type {number}
* @memberof RaucStatus
*/
percent: number
/**
* Current installation step
* @type {string}
* @memberof RaucStatus
*/
message: string
/**
* Installation error message
* @type {string}
* @memberof RaucStatus
*/
last_error: string
/**
* Full command line output of the current installation
* @type {string}
* @memberof RaucStatus
*/
log: string
}
/**
*
* @export
* @interface Rootfs
*/
export interface Rootfs {
/**
* Block device
* @type {string}
* @memberof Rootfs
*/
device: string
/**
* Filesystem
* @type {string}
* @memberof Rootfs
*/
type: string
/**
* Mount path (null when not mounted)
* @type {string}
* @memberof Rootfs
*/
mountpoint: string | null
/**
* Is the filesystem bootable
* @type {boolean}
* @memberof Rootfs
*/
bootable: boolean
/**
* Is the filesystem booted
* @type {boolean}
* @memberof Rootfs
*/
booted: boolean
/**
* Is the filesystem the next boot target
* @type {boolean}
* @memberof Rootfs
*/
primary: boolean
}
/**
*
* @export
* @interface StatusMessage
*/
export interface StatusMessage {
/**
* Is operation successful
* @type {boolean}
* @memberof StatusMessage
*/
success: boolean
/**
* Success message
* @type {string}
* @memberof StatusMessage
*/
msg: string
}
/**
*
* @export
* @interface SystemInfo
*/
export interface SystemInfo {
/**
* Hostname of the system
* @type {string}
* @memberof SystemInfo
*/
hostname: string
/**
* Name of the os distribution
* @type {string}
* @memberof SystemInfo
*/
os_name: string
/**
* Operating system version
* @type {string}
* @memberof SystemInfo
*/
os_version: string
/**
* System uptime in seconds
* @type {number}
* @memberof SystemInfo
*/
uptime: number
/**
* Compatible firmware name
* @type {string}
* @memberof SystemInfo
*/
rauc_compatible: string
/**
* Compatible firmware variant
* @type {string}
* @memberof SystemInfo
*/
rauc_variant: string
/**
* List of RAUC root filesystems
* @type {{ [key: string]: Rootfs; }}
* @memberof SystemInfo
*/
rauc_rootfs: {[key: string]: Rootfs}
}
/**
* DefaultApi - axios parameter creator
* @export
*/
export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getInfo: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/info`
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
let baseOptions
if (configuration) {
baseOptions = configuration.baseOptions
}
const localVarRequestOptions = {method: "GET", ...baseOptions, ...options}
const localVarHeaderParameter = {} as any
const localVarQueryParameter = {} as any
setSearchParams(localVarUrlObj, localVarQueryParameter)
let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {}
localVarRequestOptions.headers = {
...localVarHeaderParameter,
...headersFromBaseOptions,
...options.headers,
}
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
}
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getStatus: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/status`
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
let baseOptions
if (configuration) {
baseOptions = configuration.baseOptions
}
const localVarRequestOptions = {method: "GET", ...baseOptions, ...options}
const localVarHeaderParameter = {} as any
const localVarQueryParameter = {} as any
setSearchParams(localVarUrlObj, localVarQueryParameter)
let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {}
localVarRequestOptions.headers = {
...localVarHeaderParameter,
...headersFromBaseOptions,
...options.headers,
}
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
}
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
startReboot: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/reboot`
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
let baseOptions
if (configuration) {
baseOptions = configuration.baseOptions
}
const localVarRequestOptions = {method: "POST", ...baseOptions, ...options}
const localVarHeaderParameter = {} as any
const localVarQueryParameter = {} as any
setSearchParams(localVarUrlObj, localVarQueryParameter)
let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {}
localVarRequestOptions.headers = {
...localVarHeaderParameter,
...headersFromBaseOptions,
...options.headers,
}
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
}
},
/**
*
* @param {any} [updateFile]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
startUpdate: async (
updateFile?: any,
options: AxiosRequestConfig = {}
): Promise<RequestArgs> => {
const localVarPath = `/update`
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL)
let baseOptions
if (configuration) {
baseOptions = configuration.baseOptions
}
const localVarRequestOptions = {method: "POST", ...baseOptions, ...options}
const localVarHeaderParameter = {} as any
const localVarQueryParameter = {} as any
const localVarFormParams = new ((configuration &&
configuration.formDataCtor) ||
FormData)()
if (updateFile !== undefined) {
localVarFormParams.append("updateFile", updateFile as any)
}
localVarHeaderParameter["Content-Type"] = "multipart/form-data"
setSearchParams(localVarUrlObj, localVarQueryParameter)
let headersFromBaseOptions =
baseOptions && baseOptions.headers ? baseOptions.headers : {}
localVarRequestOptions.headers = {
...localVarHeaderParameter,
...headersFromBaseOptions,
...options.headers,
}
localVarRequestOptions.data = localVarFormParams
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
}
},
}
}
/**
* DefaultApi - functional programming interface
* @export
*/
export const DefaultApiFp = function (configuration?: Configuration) {
const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration)
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getInfo(
options?: AxiosRequestConfig
): Promise<
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemInfo>
> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getInfo(options)
return createRequestFunction(
localVarAxiosArgs,
globalAxios,
BASE_PATH,
configuration
)
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getStatus(
options?: AxiosRequestConfig
): Promise<
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RaucStatus>
> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getStatus(options)
return createRequestFunction(
localVarAxiosArgs,
globalAxios,
BASE_PATH,
configuration
)
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async startReboot(
options?: AxiosRequestConfig
): Promise<
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StatusMessage>
> {
const localVarAxiosArgs = await localVarAxiosParamCreator.startReboot(
options
)
return createRequestFunction(
localVarAxiosArgs,
globalAxios,
BASE_PATH,
configuration
)
},
/**
*
* @param {any} [updateFile]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async startUpdate(
updateFile?: any,
options?: AxiosRequestConfig
): Promise<
(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StatusMessage>
> {
const localVarAxiosArgs = await localVarAxiosParamCreator.startUpdate(
updateFile,
options
)
return createRequestFunction(
localVarAxiosArgs,
globalAxios,
BASE_PATH,
configuration
)
},
}
}
/**
* DefaultApi - factory interface
* @export
*/
export const DefaultApiFactory = function (
configuration?: Configuration,
basePath?: string,
axios?: AxiosInstance
) {
const localVarFp = DefaultApiFp(configuration)
return {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getInfo(options?: any): AxiosPromise<SystemInfo> {
return localVarFp
.getInfo(options)
.then((request) => request(axios, basePath))
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getStatus(options?: any): AxiosPromise<RaucStatus> {
return localVarFp
.getStatus(options)
.then((request) => request(axios, basePath))
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
startReboot(options?: any): AxiosPromise<StatusMessage> {
return localVarFp
.startReboot(options)
.then((request) => request(axios, basePath))
},
/**
*
* @param {any} [updateFile]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
startUpdate(updateFile?: any, options?: any): AxiosPromise<StatusMessage> {
return localVarFp
.startUpdate(updateFile, options)
.then((request) => request(axios, basePath))
},
}
}
/**
* DefaultApi - object-oriented interface
* @export
* @class DefaultApi
* @extends {BaseAPI}
*/
export class DefaultApi extends BaseAPI {
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public getInfo(options?: AxiosRequestConfig) {
return DefaultApiFp(this.configuration)
.getInfo(options)
.then((request) => request(this.axios, this.basePath))
}
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public getStatus(options?: AxiosRequestConfig) {
return DefaultApiFp(this.configuration)
.getStatus(options)
.then((request) => request(this.axios, this.basePath))
}
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public startReboot(options?: AxiosRequestConfig) {
return DefaultApiFp(this.configuration)
.startReboot(options)
.then((request) => request(this.axios, this.basePath))
}
/**
*
* @param {any} [updateFile]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof DefaultApi
*/
public startUpdate(updateFile?: any, options?: AxiosRequestConfig) {
return DefaultApiFp(this.configuration)
.startUpdate(updateFile, options)
.then((request) => request(this.axios, this.basePath))
}
}

View file

@ -0,0 +1,74 @@
/* tslint:disable */
/* eslint-disable */
/**
* SEBRAUC
* REST API for the SEBRAUC firmware updater
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import {Configuration} from "./configuration"
// Some imports not used depending on template conditions
// @ts-ignore
import globalAxios, {AxiosPromise, AxiosInstance, AxiosRequestConfig} from "axios"
export const BASE_PATH = "http://localhost:8080/api".replace(/\/+$/, "")
/**
*
* @export
*/
export const COLLECTION_FORMATS = {
csv: ",",
ssv: " ",
tsv: "\t",
pipes: "|",
}
/**
*
* @export
* @interface RequestArgs
*/
export interface RequestArgs {
url: string
options: AxiosRequestConfig
}
/**
*
* @export
* @class BaseAPI
*/
export class BaseAPI {
protected configuration: Configuration | undefined
constructor(
configuration?: Configuration,
protected basePath: string = BASE_PATH,
protected axios: AxiosInstance = globalAxios
) {
if (configuration) {
this.configuration = configuration
this.basePath = configuration.basePath || this.basePath
}
}
}
/**
*
* @export
* @class RequiredError
* @extends {Error}
*/
export class RequiredError extends Error {
name: "RequiredError" = "RequiredError"
constructor(public field: string, msg?: string) {
super(msg)
}
}

View file

@ -0,0 +1,181 @@
/* tslint:disable */
/* eslint-disable */
/**
* SEBRAUC
* REST API for the SEBRAUC firmware updater
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import {Configuration} from "./configuration"
import {RequiredError, RequestArgs} from "./base"
import {AxiosInstance, AxiosResponse} from "axios"
/**
*
* @export
*/
export const DUMMY_BASE_URL = "https://example.com"
/**
*
* @throws {RequiredError}
* @export
*/
export const assertParamExists = function (
functionName: string,
paramName: string,
paramValue: unknown
) {
if (paramValue === null || paramValue === undefined) {
throw new RequiredError(
paramName,
`Required parameter ${paramName} was null or undefined when calling ${functionName}.`
)
}
}
/**
*
* @export
*/
export const setApiKeyToObject = async function (
object: any,
keyParamName: string,
configuration?: Configuration
) {
if (configuration && configuration.apiKey) {
const localVarApiKeyValue =
typeof configuration.apiKey === "function"
? await configuration.apiKey(keyParamName)
: await configuration.apiKey
object[keyParamName] = localVarApiKeyValue
}
}
/**
*
* @export
*/
export const setBasicAuthToObject = function (
object: any,
configuration?: Configuration
) {
if (configuration && (configuration.username || configuration.password)) {
object["auth"] = {
username: configuration.username,
password: configuration.password,
}
}
}
/**
*
* @export
*/
export const setBearerAuthToObject = async function (
object: any,
configuration?: Configuration
) {
if (configuration && configuration.accessToken) {
const accessToken =
typeof configuration.accessToken === "function"
? await configuration.accessToken()
: await configuration.accessToken
object["Authorization"] = "Bearer " + accessToken
}
}
/**
*
* @export
*/
export const setOAuthToObject = async function (
object: any,
name: string,
scopes: string[],
configuration?: Configuration
) {
if (configuration && configuration.accessToken) {
const localVarAccessTokenValue =
typeof configuration.accessToken === "function"
? await configuration.accessToken(name, scopes)
: await configuration.accessToken
object["Authorization"] = "Bearer " + localVarAccessTokenValue
}
}
/**
*
* @export
*/
export const setSearchParams = function (url: URL, ...objects: any[]) {
const searchParams = new URLSearchParams(url.search)
for (const object of objects) {
for (const key in object) {
if (Array.isArray(object[key])) {
searchParams.delete(key)
for (const item of object[key]) {
searchParams.append(key, item)
}
} else {
searchParams.set(key, object[key])
}
}
}
url.search = searchParams.toString()
}
/**
*
* @export
*/
export const serializeDataIfNeeded = function (
value: any,
requestOptions: any,
configuration?: Configuration
) {
const nonString = typeof value !== "string"
const needsSerialization =
nonString && configuration && configuration.isJsonMime
? configuration.isJsonMime(requestOptions.headers["Content-Type"])
: nonString
return needsSerialization
? JSON.stringify(value !== undefined ? value : {})
: value || ""
}
/**
*
* @export
*/
export const toPathString = function (url: URL) {
return url.pathname + url.search + url.hash
}
/**
*
* @export
*/
export const createRequestFunction = function (
axiosArgs: RequestArgs,
globalAxios: AxiosInstance,
BASE_PATH: string,
configuration?: Configuration
) {
return <T = unknown, R = AxiosResponse<T>>(
axios: AxiosInstance = globalAxios,
basePath: string = BASE_PATH
) => {
const axiosRequestArgs = {
...axiosArgs.options,
url: (configuration?.basePath || basePath) + axiosArgs.url,
}
return axios.request<T, R>(axiosRequestArgs)
}
}

View file

@ -0,0 +1,123 @@
/* tslint:disable */
/* eslint-disable */
/**
* SEBRAUC
* REST API for the SEBRAUC firmware updater
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface ConfigurationParameters {
apiKey?:
| string
| Promise<string>
| ((name: string) => string)
| ((name: string) => Promise<string>)
username?: string
password?: string
accessToken?:
| string
| Promise<string>
| ((name?: string, scopes?: string[]) => string)
| ((name?: string, scopes?: string[]) => Promise<string>)
basePath?: string
baseOptions?: any
formDataCtor?: new () => any
}
export class Configuration {
/**
* parameter for apiKey security
* @param name security name
* @memberof Configuration
*/
apiKey?:
| string
| Promise<string>
| ((name: string) => string)
| ((name: string) => Promise<string>)
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
username?: string
/**
* parameter for basic security
*
* @type {string}
* @memberof Configuration
*/
password?: string
/**
* parameter for oauth2 security
* @param name security name
* @param scopes oauth2 scope
* @memberof Configuration
*/
accessToken?:
| string
| Promise<string>
| ((name?: string, scopes?: string[]) => string)
| ((name?: string, scopes?: string[]) => Promise<string>)
/**
* override base path
*
* @type {string}
* @memberof Configuration
*/
basePath?: string
/**
* base options for axios calls
*
* @type {any}
* @memberof Configuration
*/
baseOptions?: any
/**
* The FormData constructor that will be used to create multipart form data
* requests. You can inject this here so that execution environments that
* do not support the FormData class can still run the generated client.
*
* @type {new () => FormData}
*/
formDataCtor?: new () => any
constructor(param: ConfigurationParameters = {}) {
this.apiKey = param.apiKey
this.username = param.username
this.password = param.password
this.accessToken = param.accessToken
this.basePath = param.basePath
this.baseOptions = param.baseOptions
this.formDataCtor = param.formDataCtor
}
/**
* Check if the given MIME is a JSON MIME.
* JSON MIME examples:
* application/json
* application/json; charset=UTF8
* APPLICATION/JSON
* application/vnd.company+json
* @param mime - MIME (Multipurpose Internet Mail Extensions)
* @return True if the given MIME is JSON, false otherwise.
*/
public isJsonMime(mime: string): boolean {
const jsonMime: RegExp = new RegExp(
"^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$",
"i"
)
return (
mime !== null &&
(jsonMime.test(mime) ||
mime.toLowerCase() === "application/json-patch+json")
)
}
}

View file

@ -0,0 +1,16 @@
/* tslint:disable */
/* eslint-disable */
/**
* SEBRAUC
* REST API for the SEBRAUC firmware updater
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export * from "./api"
export * from "./configuration"

View file

@ -1,3 +1,5 @@
@use "table";
html, html,
body { body {
height: 100%; height: 100%;

47
ui/src/style/table.scss Normal file
View file

@ -0,0 +1,47 @@
.table-wrapper {
width: 100%;
overflow-x: auto;
}
.table {
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
border-collapse: collapse;
margin: 0;
padding: 0;
width: 100%;
&.no-bottom-border {
&,
> tr:last-child,
:not(thead) tr:last-child {
border-bottom: none;
}
}
}
.table caption {
font-size: 1.5em;
margin: 0.5em 0 0.75em;
}
.table tr {
border-bottom: 1px solid #ddd;
padding: 0.35em;
}
.table th,
.table td {
padding: 0.625em;
text-align: left;
.icon {
color: #1f85de;
}
}
.table th {
font-size: 0.85em;
letter-spacing: 0.085em;
text-transform: uppercase;
}

View file

@ -1,3 +1,5 @@
import {Configuration, DefaultApi} from "../sebrauc-client"
let apiHost = document.location.host let apiHost = document.location.host
const httpProto = document.location.protocol const httpProto = document.location.protocol
const wsProto = httpProto === "https:" ? "wss:" : "ws:" const wsProto = httpProto === "https:" ? "wss:" : "ws:"
@ -9,4 +11,10 @@ if (import.meta.env.VITE_API_HOST !== undefined) {
const apiUrl = `${httpProto}//${apiHost}/api` const apiUrl = `${httpProto}//${apiHost}/api`
const wsUrl = `${wsProto}//${apiHost}/api/ws` const wsUrl = `${wsProto}//${apiHost}/api/ws`
export {apiUrl, wsUrl} let apicfg = new Configuration({
basePath: apiUrl,
})
const sebraucApi = new DefaultApi(apicfg)
export {apiUrl, wsUrl, sebraucApi}

7
ui/src/util/colors.ts Normal file
View file

@ -0,0 +1,7 @@
class colors {
static readonly RED = "#FF0039"
static readonly GREEN = "#148420"
static readonly BLUE = "#1f85de"
}
export default colors

18
ui/src/util/functions.ts Normal file
View file

@ -0,0 +1,18 @@
function secondsToString(seconds: number): string {
const numyears = Math.floor(seconds / 31536000)
const numdays = Math.floor((seconds % 31536000) / 86400)
const numhours = Math.floor(((seconds % 31536000) % 86400) / 3600)
const numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60)
const numseconds = (((seconds % 31536000) % 86400) % 3600) % 60
let res = []
if (numyears > 0) res.push(numyears + "yr")
if (numdays > 0) res.push(numdays + "d")
if (numhours > 0) res.push(numhours + "h")
if (numminutes > 0) res.push(numminutes + "m")
if (seconds < 60) res.push(numseconds + "s")
return res.join(" ")
}
export {secondsToString}