mirror of https://github.com/jetkvm/kvm.git
Compare commits
1 Commits
ed9dba8230
...
c5d6fb4d38
Author | SHA1 | Date |
---|---|---|
|
c5d6fb4d38 |
30
config.go
30
config.go
|
@ -12,30 +12,24 @@ type WakeOnLanDevice struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
CloudURL string `json:"cloud_url"`
|
CloudURL string `json:"cloud_url"`
|
||||||
CloudToken string `json:"cloud_token"`
|
CloudToken string `json:"cloud_token"`
|
||||||
GoogleIdentity string `json:"google_identity"`
|
GoogleIdentity string `json:"google_identity"`
|
||||||
JigglerEnabled bool `json:"jiggler_enabled"`
|
JigglerEnabled bool `json:"jiggler_enabled"`
|
||||||
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
||||||
IncludePreRelease bool `json:"include_pre_release"`
|
IncludePreRelease bool `json:"include_pre_release"`
|
||||||
HashedPassword string `json:"hashed_password"`
|
HashedPassword string `json:"hashed_password"`
|
||||||
LocalAuthToken string `json:"local_auth_token"`
|
LocalAuthToken string `json:"local_auth_token"`
|
||||||
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
||||||
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
||||||
EdidString string `json:"hdmi_edid_string"`
|
EdidString string `json:"hdmi_edid_string"`
|
||||||
DisplayMaxBrightness int `json:"display_max_brightness"`
|
|
||||||
DisplayDimAfterSec int `json:"display_dim_after_sec"`
|
|
||||||
DisplayOffAfterSec int `json:"display_off_after_sec"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const configPath = "/userdata/kvm_config.json"
|
const configPath = "/userdata/kvm_config.json"
|
||||||
|
|
||||||
var defaultConfig = &Config{
|
var defaultConfig = &Config{
|
||||||
CloudURL: "https://api.jetkvm.com",
|
CloudURL: "https://api.jetkvm.com",
|
||||||
AutoUpdateEnabled: true, // Set a default value
|
AutoUpdateEnabled: true, // Set a default value
|
||||||
DisplayMaxBrightness: 64,
|
|
||||||
DisplayDimAfterSec: 120, // 2 minutes
|
|
||||||
DisplayOffAfterSec: 1800, // 30 minutes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var config *Config
|
var config *Config
|
||||||
|
|
169
display.go
169
display.go
|
@ -1,26 +1,12 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var currentScreen = "ui_Boot_Screen"
|
var currentScreen = "ui_Boot_Screen"
|
||||||
var backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF
|
|
||||||
|
|
||||||
var (
|
|
||||||
dimTicker *time.Ticker
|
|
||||||
offTicker *time.Ticker
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
touchscreenDevice string = "/dev/input/event1"
|
|
||||||
backlightControlClass string = "/sys/class/backlight/backlight/brightness"
|
|
||||||
)
|
|
||||||
|
|
||||||
func switchToScreen(screen string) {
|
func switchToScreen(screen string) {
|
||||||
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
|
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
|
||||||
|
@ -79,7 +65,6 @@ func requestDisplayUpdate() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
wakeDisplay(false)
|
|
||||||
fmt.Println("display updating........................")
|
fmt.Println("display updating........................")
|
||||||
//TODO: only run once regardless how many pending updates
|
//TODO: only run once regardless how many pending updates
|
||||||
updateDisplay()
|
updateDisplay()
|
||||||
|
@ -98,156 +83,6 @@ func updateStaticContents() {
|
||||||
updateLabelIfChanged("ui_Status_Content_Device_Id_Content_Label", GetDeviceID())
|
updateLabelIfChanged("ui_Status_Content_Device_Id_Content_Label", GetDeviceID())
|
||||||
}
|
}
|
||||||
|
|
||||||
// setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter
|
|
||||||
// the backlight brightness of the JetKVM hardware's display.
|
|
||||||
func setDisplayBrightness(brightness int) error {
|
|
||||||
// NOTE: The actual maximum value for this is 255, but out-of-the-box, the value is set to 64.
|
|
||||||
// The maximum set here is set to 100 to reduce the risk of drawing too much power (and besides, 255 is very bright!).
|
|
||||||
if brightness > 100 || brightness < 0 {
|
|
||||||
return errors.New("brightness value out of bounds, must be between 0 and 100")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the display backlight class is available
|
|
||||||
if _, err := os.Stat(backlightControlClass); errors.Is(err, os.ErrNotExist) {
|
|
||||||
return errors.New("brightness value cannot be set, possibly not running on JetKVM hardware")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the value
|
|
||||||
bs := []byte(strconv.Itoa(brightness))
|
|
||||||
err := os.WriteFile(backlightControlClass, bs, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("display: set brightness to %v\n", brightness)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// tick_displayDim() is called when when dim ticker expires, it simply reduces the brightness
|
|
||||||
// of the display by half of the max brightness.
|
|
||||||
func tick_displayDim() {
|
|
||||||
err := setDisplayBrightness(config.DisplayMaxBrightness / 2)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("display: failed to dim display: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dimTicker.Stop()
|
|
||||||
|
|
||||||
backlightState = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// tick_displayOff() is called when the off ticker expires, it turns off the display
|
|
||||||
// by setting the brightness to zero.
|
|
||||||
func tick_displayOff() {
|
|
||||||
err := setDisplayBrightness(0)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("display: failed to turn off display: %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
offTicker.Stop()
|
|
||||||
|
|
||||||
backlightState = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// wakeDisplay sets the display brightness back to config.DisplayMaxBrightness and stores the time the display
|
|
||||||
// last woke, ready for displayTimeoutTick to put the display back in the dim/off states.
|
|
||||||
// Set force to true to skip the backlight state check, this should be done if altering the tickers.
|
|
||||||
func wakeDisplay(force bool) {
|
|
||||||
if backlightState == 0 && !force {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't try to wake up if the display is turned off.
|
|
||||||
if config.DisplayMaxBrightness == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := setDisplayBrightness(config.DisplayMaxBrightness)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("display wake failed, %s\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.DisplayDimAfterSec != 0 {
|
|
||||||
dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.DisplayOffAfterSec != 0 {
|
|
||||||
offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
|
||||||
}
|
|
||||||
backlightState = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// watchTsEvents monitors the touchscreen for events and simply calls wakeDisplay() to ensure the
|
|
||||||
// touchscreen interface still works even with LCD dimming/off.
|
|
||||||
// TODO: This is quite a hack, really we should be getting an event from jetkvm_native, or the whole display backlight
|
|
||||||
// control should be hoisted up to jetkvm_native.
|
|
||||||
func watchTsEvents() {
|
|
||||||
ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("display: failed to open touchscreen device: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
// This buffer is set to 24 bytes as that's the normal size of events on /dev/input
|
|
||||||
// Reference: https://www.kernel.org/doc/Documentation/input/input.txt
|
|
||||||
// This could potentially be set higher, to require multiple events to wake the display.
|
|
||||||
buf := make([]byte, 24)
|
|
||||||
for {
|
|
||||||
_, err := ts.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("display: failed to read from touchscreen device: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
wakeDisplay(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// startBacklightTickers starts the two tickers for dimming and switching off the display
|
|
||||||
// if they're not already set. This is done separately to the init routine as the "never dim"
|
|
||||||
// option has the value set to zero, but time.NewTicker only accept positive values.
|
|
||||||
func startBacklightTickers() {
|
|
||||||
LoadConfig()
|
|
||||||
// Don't start the tickers if the display is switched off.
|
|
||||||
// Set the display to off if that's the case.
|
|
||||||
if config.DisplayMaxBrightness == 0 {
|
|
||||||
setDisplayBrightness(0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if dimTicker == nil && config.DisplayDimAfterSec != 0 {
|
|
||||||
fmt.Printf("display: dim_ticker has started\n")
|
|
||||||
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
|
||||||
defer dimTicker.Stop()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-dimTicker.C:
|
|
||||||
tick_displayDim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
if offTicker == nil && config.DisplayOffAfterSec != 0 {
|
|
||||||
fmt.Printf("display: off_ticker has started\n")
|
|
||||||
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
|
||||||
defer offTicker.Stop()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-offTicker.C:
|
|
||||||
tick_displayOff()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
go func() {
|
go func() {
|
||||||
waitCtrlClientConnected()
|
waitCtrlClientConnected()
|
||||||
|
@ -256,10 +91,6 @@ func init() {
|
||||||
updateStaticContents()
|
updateStaticContents()
|
||||||
displayInited = true
|
displayInited = true
|
||||||
fmt.Println("display inited")
|
fmt.Println("display inited")
|
||||||
startBacklightTickers()
|
|
||||||
wakeDisplay(true)
|
|
||||||
requestDisplayUpdate()
|
requestDisplayUpdate()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go watchTsEvents()
|
|
||||||
}
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -14,7 +14,6 @@ require (
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
|
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1
|
github.com/hanwen/go-fuse/v2 v2.5.1
|
||||||
github.com/hashicorp/go-envparse v0.1.0
|
|
||||||
github.com/openstadia/go-usb-gadget v0.0.0-20231115171102-aebd56bbb965
|
github.com/openstadia/go-usb-gadget v0.0.0-20231115171102-aebd56bbb965
|
||||||
github.com/pion/logging v0.2.2
|
github.com/pion/logging v0.2.2
|
||||||
github.com/pion/mdns/v2 v2.0.7
|
github.com/pion/mdns/v2 v2.0.7
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -49,8 +49,6 @@ github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto
|
||||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ=
|
github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ=
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
|
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
|
||||||
github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY=
|
|
||||||
github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
|
60
jsonrpc.go
60
jsonrpc.go
|
@ -34,12 +34,6 @@ type JSONRPCEvent struct {
|
||||||
Params interface{} `json:"params,omitempty"`
|
Params interface{} `json:"params,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BacklightSettings struct {
|
|
||||||
MaxBrightness int `json:"max_brightness"`
|
|
||||||
DimAfter int `json:"dim_after"`
|
|
||||||
OffAfter int `json:"off_after"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeJSONRPCResponse(response JSONRPCResponse, session *Session) {
|
func writeJSONRPCResponse(response JSONRPCResponse, session *Session) {
|
||||||
responseBytes, err := json.Marshal(response)
|
responseBytes, err := json.Marshal(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -231,56 +225,6 @@ func rpcTryUpdate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetBacklightSettings(params BacklightSettings) error {
|
|
||||||
LoadConfig()
|
|
||||||
|
|
||||||
blConfig := params
|
|
||||||
|
|
||||||
// NOTE: by default, the frontend limits the brightness to 64, as that's what the device originally shipped with.
|
|
||||||
if blConfig.MaxBrightness > 255 || blConfig.MaxBrightness < 0 {
|
|
||||||
return fmt.Errorf("maxBrightness must be between 0 and 255")
|
|
||||||
}
|
|
||||||
|
|
||||||
if blConfig.DimAfter < 0 {
|
|
||||||
return fmt.Errorf("dimAfter must be a positive integer")
|
|
||||||
}
|
|
||||||
|
|
||||||
if blConfig.OffAfter < 0 {
|
|
||||||
return fmt.Errorf("offAfter must be a positive integer")
|
|
||||||
}
|
|
||||||
|
|
||||||
config.DisplayMaxBrightness = blConfig.MaxBrightness
|
|
||||||
config.DisplayDimAfterSec = blConfig.DimAfter
|
|
||||||
config.DisplayOffAfterSec = blConfig.OffAfter
|
|
||||||
|
|
||||||
if err := SaveConfig(); err != nil {
|
|
||||||
return fmt.Errorf("failed to save config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("rpc: display: settings applied, max_brightness: %d, dim after: %ds, off after: %ds", config.DisplayMaxBrightness, config.DisplayDimAfterSec, config.DisplayOffAfterSec)
|
|
||||||
|
|
||||||
// If the device started up with auto-dim and/or auto-off set to zero, the display init
|
|
||||||
// method will not have started the tickers. So in case that has changed, attempt to start the tickers now.
|
|
||||||
startBacklightTickers()
|
|
||||||
|
|
||||||
// Wake the display after the settings are altered, this ensures the tickers
|
|
||||||
// are reset to the new settings, and will bring the display up to maxBrightness.
|
|
||||||
// Calling with force set to true, to ignore the current state of the display, and force
|
|
||||||
// it to reset the tickers.
|
|
||||||
wakeDisplay(true)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func rpcGetBacklightSettings() (*BacklightSettings, error) {
|
|
||||||
LoadConfig()
|
|
||||||
|
|
||||||
return &BacklightSettings{
|
|
||||||
MaxBrightness: config.DisplayMaxBrightness,
|
|
||||||
DimAfter: int(config.DisplayDimAfterSec),
|
|
||||||
OffAfter: int(config.DisplayOffAfterSec),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
devModeFile = "/userdata/jetkvm/devmode.enable"
|
devModeFile = "/userdata/jetkvm/devmode.enable"
|
||||||
sshKeyDir = "/userdata/dropbear/.ssh"
|
sshKeyDir = "/userdata/dropbear/.ssh"
|
||||||
|
@ -441,7 +385,7 @@ func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interfac
|
||||||
}
|
}
|
||||||
args[i] = reflect.ValueOf(newStruct).Elem()
|
args[i] = reflect.ValueOf(newStruct).Elem()
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("invalid parameter type for: %s, type: %s", paramName, paramType.Kind())
|
return nil, fmt.Errorf("invalid parameter type for: %s", paramName)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
args[i] = convertedValue.Convert(paramType)
|
args[i] = convertedValue.Convert(paramType)
|
||||||
|
@ -616,6 +560,4 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
|
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
|
||||||
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
|
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
|
||||||
"resetConfig": {Func: rpcResetConfig},
|
"resetConfig": {Func: rpcResetConfig},
|
||||||
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
|
|
||||||
"getBacklightSettings": {Func: rpcGetBacklightSettings},
|
|
||||||
}
|
}
|
||||||
|
|
83
network.go
83
network.go
|
@ -1,19 +1,13 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-envparse"
|
|
||||||
"github.com/pion/mdns/v2"
|
"github.com/pion/mdns/v2"
|
||||||
"golang.org/x/net/ipv4"
|
"golang.org/x/net/ipv4"
|
||||||
"golang.org/x/net/ipv6"
|
"golang.org/x/net/ipv6"
|
||||||
|
"net"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
"github.com/vishvananda/netlink/nl"
|
"github.com/vishvananda/netlink/nl"
|
||||||
|
@ -21,15 +15,11 @@ import (
|
||||||
|
|
||||||
var mDNSConn *mdns.Conn
|
var mDNSConn *mdns.Conn
|
||||||
|
|
||||||
var networkState NetworkState
|
var networkState struct {
|
||||||
|
|
||||||
type NetworkState struct {
|
|
||||||
Up bool
|
Up bool
|
||||||
IPv4 string
|
IPv4 string
|
||||||
IPv6 string
|
IPv6 string
|
||||||
MAC string
|
MAC string
|
||||||
|
|
||||||
checked bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalIpInfo struct {
|
type LocalIpInfo struct {
|
||||||
|
@ -38,45 +28,43 @@ type LocalIpInfo struct {
|
||||||
MAC string
|
MAC string
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
NetIfName = "eth0"
|
|
||||||
DHCPLeaseFile = "/run/udhcpc.%s.info"
|
|
||||||
)
|
|
||||||
|
|
||||||
// setDhcpClientState sends signals to udhcpc to change it's current mode
|
// setDhcpClientState sends signals to udhcpc to change it's current mode
|
||||||
// of operation. Setting active to true will force udhcpc to renew the DHCP lease.
|
// of operation. Setting active to true will force udhcpc to renew the DHCP lease.
|
||||||
// Setting active to false will put udhcpc into idle mode.
|
// Setting active to false will put udhcpc into idle mode.
|
||||||
func setDhcpClientState(active bool) {
|
func setDhcpClientState(active bool) {
|
||||||
var signal string
|
var signal string;
|
||||||
if active {
|
if active {
|
||||||
signal = "-SIGUSR1"
|
signal = "-SIGUSR1"
|
||||||
} else {
|
} else {
|
||||||
signal = "-SIGUSR2"
|
signal = "-SIGUSR2"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("/usr/bin/killall", signal, "udhcpc")
|
cmd := exec.Command("/usr/bin/killall", signal, "udhcpc");
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
fmt.Printf("network: setDhcpClientState: failed to change udhcpc state: %s\n", err)
|
fmt.Printf("network: setDhcpClientState: failed to change udhcpc state: %s\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkNetworkState() {
|
func checkNetworkState() {
|
||||||
iface, err := netlink.LinkByName(NetIfName)
|
iface, err := netlink.LinkByName("eth0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to get [%s] interface: %v\n", NetIfName, err)
|
fmt.Printf("failed to get eth0 interface: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newState := NetworkState{
|
newState := struct {
|
||||||
|
Up bool
|
||||||
|
IPv4 string
|
||||||
|
IPv6 string
|
||||||
|
MAC string
|
||||||
|
}{
|
||||||
Up: iface.Attrs().OperState == netlink.OperUp,
|
Up: iface.Attrs().OperState == netlink.OperUp,
|
||||||
MAC: iface.Attrs().HardwareAddr.String(),
|
MAC: iface.Attrs().HardwareAddr.String(),
|
||||||
|
|
||||||
checked: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL)
|
addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to get addresses for [%s]: %v\n", NetIfName, err)
|
fmt.Printf("failed to get addresses for eth0: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the link is going down, put udhcpc into idle mode.
|
// If the link is going down, put udhcpc into idle mode.
|
||||||
|
@ -106,7 +94,7 @@ func checkNetworkState() {
|
||||||
|
|
||||||
if newState != networkState {
|
if newState != networkState {
|
||||||
fmt.Println("network state changed")
|
fmt.Println("network state changed")
|
||||||
// restart MDNS
|
//restart MDNS
|
||||||
startMDNS()
|
startMDNS()
|
||||||
networkState = newState
|
networkState = newState
|
||||||
requestDisplayUpdate()
|
requestDisplayUpdate()
|
||||||
|
@ -114,7 +102,7 @@ func checkNetworkState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func startMDNS() error {
|
func startMDNS() error {
|
||||||
// If server was previously running, stop it
|
//If server was previously running, stop it
|
||||||
if mDNSConn != nil {
|
if mDNSConn != nil {
|
||||||
fmt.Printf("Stopping mDNS server\n")
|
fmt.Printf("Stopping mDNS server\n")
|
||||||
err := mDNSConn.Close()
|
err := mDNSConn.Close()
|
||||||
|
@ -123,7 +111,7 @@ func startMDNS() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new server
|
//Start a new server
|
||||||
fmt.Printf("Starting mDNS server on jetkvm.local\n")
|
fmt.Printf("Starting mDNS server on jetkvm.local\n")
|
||||||
addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4)
|
addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -156,39 +144,6 @@ func startMDNS() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNTPServersFromDHCPInfo() ([]string, error) {
|
|
||||||
buf, err := os.ReadFile(fmt.Sprintf(DHCPLeaseFile, NetIfName))
|
|
||||||
if err != nil {
|
|
||||||
// do not return error if file does not exist
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("failed to load udhcpc info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse udhcpc info
|
|
||||||
env, err := envparse.Parse(bytes.NewReader(buf))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse udhcpc info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
val, ok := env["ntpsrv"]
|
|
||||||
if !ok {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var servers []string
|
|
||||||
|
|
||||||
for _, server := range strings.Fields(val) {
|
|
||||||
if net.ParseIP(server) == nil {
|
|
||||||
fmt.Printf("invalid NTP server IP: %s, ignoring ... \n", server)
|
|
||||||
}
|
|
||||||
servers = append(servers, server)
|
|
||||||
}
|
|
||||||
|
|
||||||
return servers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
updates := make(chan netlink.LinkUpdate)
|
updates := make(chan netlink.LinkUpdate)
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
@ -207,7 +162,7 @@ func init() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case update := <-updates:
|
case update := <-updates:
|
||||||
if update.Link.Attrs().Name == NetIfName {
|
if update.Link.Attrs().Name == "eth0" {
|
||||||
fmt.Printf("link update: %+v\n", update)
|
fmt.Printf("link update: %+v\n", update)
|
||||||
checkNetworkState()
|
checkNetworkState()
|
||||||
}
|
}
|
||||||
|
|
61
ntp.go
61
ntp.go
|
@ -11,56 +11,20 @@ import (
|
||||||
"github.com/beevik/ntp"
|
"github.com/beevik/ntp"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var timeSynced = false
|
||||||
timeSyncRetryStep = 5 * time.Second
|
|
||||||
timeSyncRetryMaxInt = 1 * time.Minute
|
|
||||||
timeSyncWaitNetChkInt = 100 * time.Millisecond
|
|
||||||
timeSyncWaitNetUpInt = 3 * time.Second
|
|
||||||
timeSyncInterval = 1 * time.Hour
|
|
||||||
timeSyncTimeout = 2 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
timeSynced = false
|
|
||||||
timeSyncRetryInterval = 0 * time.Second
|
|
||||||
defaultNTPServers = []string{
|
|
||||||
"time.cloudflare.com",
|
|
||||||
"time.apple.com",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func TimeSyncLoop() {
|
func TimeSyncLoop() {
|
||||||
for {
|
for {
|
||||||
if !networkState.checked {
|
fmt.Println("Syncing system time")
|
||||||
time.Sleep(timeSyncWaitNetChkInt)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !networkState.Up {
|
|
||||||
log.Printf("Waiting for network to come up")
|
|
||||||
time.Sleep(timeSyncWaitNetUpInt)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Syncing system time")
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
err := SyncSystemTime()
|
err := SyncSystemTime()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to sync system time: %v", err)
|
log.Printf("Failed to sync system time: %v", err)
|
||||||
|
|
||||||
// retry after a delay
|
|
||||||
timeSyncRetryInterval += timeSyncRetryStep
|
|
||||||
time.Sleep(timeSyncRetryInterval)
|
|
||||||
// reset the retry interval if it exceeds the max interval
|
|
||||||
if timeSyncRetryInterval > timeSyncRetryMaxInt {
|
|
||||||
timeSyncRetryInterval = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start))
|
log.Printf("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start))
|
||||||
timeSynced = true
|
timeSynced = true
|
||||||
time.Sleep(timeSyncInterval) // after the first sync is done
|
time.Sleep(1 * time.Hour) //once the first sync is done, sync every hour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,22 +41,13 @@ func SyncSystemTime() (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryNetworkTime() (*time.Time, error) {
|
func queryNetworkTime() (*time.Time, error) {
|
||||||
ntpServers, err := getNTPServersFromDHCPInfo()
|
ntpServers := []string{
|
||||||
if err != nil {
|
"time.cloudflare.com",
|
||||||
log.Printf("failed to get NTP servers from DHCP info: %v\n", err)
|
"time.apple.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
if ntpServers == nil {
|
|
||||||
ntpServers = defaultNTPServers
|
|
||||||
log.Printf("Using default NTP servers: %v\n", ntpServers)
|
|
||||||
} else {
|
|
||||||
log.Printf("Using NTP servers from DHCP: %v\n", ntpServers)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, server := range ntpServers {
|
for _, server := range ntpServers {
|
||||||
now, err := queryNtpServer(server, timeSyncTimeout)
|
now, err := queryNtpServer(server, 2*time.Second)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Printf("NTP server [%s] returned time: %v\n", server, now)
|
|
||||||
return now, nil
|
return now, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,7 +56,7 @@ func queryNetworkTime() (*time.Time, error) {
|
||||||
"http://cloudflare.com",
|
"http://cloudflare.com",
|
||||||
}
|
}
|
||||||
for _, url := range httpUrls {
|
for _, url := range httpUrls {
|
||||||
now, err := queryHttpTime(url, timeSyncTimeout)
|
now, err := queryHttpTime(url, 2*time.Second)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return now, nil
|
return now, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,5 +2,3 @@ VITE_SIGNAL_API=http://localhost:3000
|
||||||
|
|
||||||
VITE_CLOUD_APP=http://localhost:5173
|
VITE_CLOUD_APP=http://localhost:5173
|
||||||
VITE_CLOUD_API=http://localhost:3000
|
VITE_CLOUD_API=http://localhost:3000
|
||||||
|
|
||||||
VITE_JETKVM_HEAD=
|
|
|
@ -2,5 +2,3 @@ VITE_SIGNAL_API= # Uses the KVM device's IP address as the signal API endpoint
|
||||||
|
|
||||||
VITE_CLOUD_APP=https://app.jetkvm.com
|
VITE_CLOUD_APP=https://app.jetkvm.com
|
||||||
VITE_CLOUD_API=https://api.jetkvm.com
|
VITE_CLOUD_API=https://api.jetkvm.com
|
||||||
|
|
||||||
VITE_JETKVM_HEAD=<script src="/device/ui-config.js"></script>
|
|
|
@ -1,6 +1,4 @@
|
||||||
VITE_SIGNAL_API=https://api.jetkvm.com
|
VITE_SIGNAL_API=https://api.jetkvm.com
|
||||||
|
|
||||||
VITE_CLOUD_APP=https://app.jetkvm.com
|
VITE_CLOUD_APP=https://app.jetkvm.com
|
||||||
VITE_CLOUD_API=https://api.jetkvm.com
|
VITE_CLOUD_API=https://api.jetkvm.com
|
||||||
|
|
||||||
VITE_JETKVM_HEAD=
|
|
|
@ -28,7 +28,6 @@
|
||||||
<title>JetKVM</title>
|
<title>JetKVM</title>
|
||||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" href="/favicon.png" />
|
||||||
%VITE_JETKVM_HEAD%
|
|
||||||
<script>
|
<script>
|
||||||
// Initial theme setup
|
// Initial theme setup
|
||||||
document.documentElement.classList.toggle(
|
document.documentElement.classList.toggle(
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { useLocation, useNavigation, useSearchParams } from "react-router-dom";
|
||||||
import Fieldset from "@components/Fieldset";
|
import Fieldset from "@components/Fieldset";
|
||||||
import GridBackground from "@components/GridBackground";
|
import GridBackground from "@components/GridBackground";
|
||||||
import StepCounter from "@components/StepCounter";
|
import StepCounter from "@components/StepCounter";
|
||||||
import { CLOUD_API } from "@/ui.config";
|
|
||||||
|
|
||||||
type AuthLayoutProps = {
|
type AuthLayoutProps = {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -63,7 +62,7 @@ export default function AuthLayout({
|
||||||
<Fieldset className="space-y-12">
|
<Fieldset className="space-y-12">
|
||||||
<div className="max-w-sm mx-auto space-y-4">
|
<div className="max-w-sm mx-auto space-y-4">
|
||||||
<form
|
<form
|
||||||
action={`${CLOUD_API}/oidc/google`}
|
action={`${import.meta.env.VITE_CLOUD_API}/oidc/google`}
|
||||||
method="POST"
|
method="POST"
|
||||||
>
|
>
|
||||||
{/*This could be the KVM ID*/}
|
{/*This could be the KVM ID*/}
|
||||||
|
|
|
@ -14,7 +14,6 @@ import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { isOnDevice } from "../main";
|
import { isOnDevice } from "../main";
|
||||||
import { Button, LinkButton } from "./Button";
|
import { Button, LinkButton } from "./Button";
|
||||||
import { CLOUD_API, SIGNAL_API } from "@/ui.config";
|
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
|
@ -38,8 +37,8 @@ export default function DashboardNavbar({
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const onLogout = useCallback(async () => {
|
const onLogout = useCallback(async () => {
|
||||||
const logoutUrl = isOnDevice
|
const logoutUrl = isOnDevice
|
||||||
? `${SIGNAL_API}/auth/logout`
|
? `${import.meta.env.VITE_SIGNAL_API}/auth/logout`
|
||||||
: `${CLOUD_API}/logout`;
|
: `${import.meta.env.VITE_CLOUD_API}/logout`;
|
||||||
const res = await api.POST(logoutUrl);
|
const res = await api.POST(logoutUrl);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,6 @@ import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
||||||
import notifications from "../notifications";
|
import notifications from "../notifications";
|
||||||
import Fieldset from "./Fieldset";
|
import Fieldset from "./Fieldset";
|
||||||
import { isOnDevice } from "../main";
|
import { isOnDevice } from "../main";
|
||||||
import { SIGNAL_API } from "@/ui.config";
|
|
||||||
|
|
||||||
export default function MountMediaModal({
|
export default function MountMediaModal({
|
||||||
open,
|
open,
|
||||||
|
@ -1120,7 +1119,7 @@ function UploadFileView({
|
||||||
alreadyUploadedBytes: number,
|
alreadyUploadedBytes: number,
|
||||||
dataChannel: string,
|
dataChannel: string,
|
||||||
) {
|
) {
|
||||||
const uploadUrl = `${SIGNAL_API}/storage/upload?uploadId=${dataChannel}`;
|
const uploadUrl = `${import.meta.env.VITE_SIGNAL_API}/storage/upload?uploadId=${dataChannel}`;
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open("POST", uploadUrl, true);
|
xhr.open("POST", uploadUrl, true);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import SidebarHeader from "@components/SidebarHeader";
|
import SidebarHeader from "@components/SidebarHeader";
|
||||||
import {
|
import {
|
||||||
BacklightSettings,
|
|
||||||
useLocalAuthModalStore,
|
useLocalAuthModalStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useUiStore,
|
useUiStore,
|
||||||
|
@ -26,7 +25,6 @@ import LocalAuthPasswordDialog from "@/components/LocalAuthPasswordDialog";
|
||||||
import { LocalDevice } from "@routes/devices.$id";
|
import { LocalDevice } from "@routes/devices.$id";
|
||||||
import { useRevalidator } from "react-router-dom";
|
import { useRevalidator } from "react-router-dom";
|
||||||
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
import { ShieldCheckIcon } from "@heroicons/react/20/solid";
|
||||||
import { CLOUD_APP, SIGNAL_API } from "@/ui.config";
|
|
||||||
|
|
||||||
export function SettingsItem({
|
export function SettingsItem({
|
||||||
title,
|
title,
|
||||||
|
@ -97,7 +95,6 @@ export default function SettingsSidebar() {
|
||||||
const hideCursor = useSettingsStore(state => state.isCursorHidden);
|
const hideCursor = useSettingsStore(state => state.isCursorHidden);
|
||||||
const setHideCursor = useSettingsStore(state => state.setCursorVisibility);
|
const setHideCursor = useSettingsStore(state => state.setCursorVisibility);
|
||||||
const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode);
|
const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode);
|
||||||
const setBacklightSettings = useSettingsStore(state => state.setBacklightSettings);
|
|
||||||
|
|
||||||
const [currentVersions, setCurrentVersions] = useState<{
|
const [currentVersions, setCurrentVersions] = useState<{
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
|
@ -231,28 +228,6 @@ export default function SettingsSidebar() {
|
||||||
[send, setDeveloperMode],
|
[send, setDeveloperMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBacklightSettingsChange = (settings: BacklightSettings) => {
|
|
||||||
// If the user has set the display to dim after it turns off, set the dim_after
|
|
||||||
// value to never.
|
|
||||||
if (settings.dim_after > settings.off_after && settings.off_after != 0) {
|
|
||||||
settings.dim_after = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
setBacklightSettings(settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBacklightSettingsSave = () => {
|
|
||||||
send("setBacklightSettings", { params: settings.backlightSettings }, resp => {
|
|
||||||
if ("error" in resp) {
|
|
||||||
notifications.error(
|
|
||||||
`Failed to set backlight settings: ${resp.error.data || "Unknown error"}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
notifications.success("Backlight settings updated successfully");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateSSHKey = useCallback(() => {
|
const handleUpdateSSHKey = useCallback(() => {
|
||||||
send("setSSHKeyState", { sshKey }, resp => {
|
send("setSSHKeyState", { sshKey }, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
|
@ -327,17 +302,6 @@ export default function SettingsSidebar() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
send("getBacklightSettings", {}, resp => {
|
|
||||||
if ("error" in resp) {
|
|
||||||
notifications.error(
|
|
||||||
`Failed to get backlight settings: ${resp.error.data || "Unknown error"}`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = resp.result as BacklightSettings;
|
|
||||||
setBacklightSettings(result);
|
|
||||||
})
|
|
||||||
|
|
||||||
send("getDevModeState", {}, resp => {
|
send("getDevModeState", {}, resp => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
const result = resp.result as { enabled: boolean };
|
const result = resp.result as { enabled: boolean };
|
||||||
|
@ -367,7 +331,7 @@ export default function SettingsSidebar() {
|
||||||
const getDevice = useCallback(async () => {
|
const getDevice = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const status = await api
|
const status = await api
|
||||||
.GET(`${SIGNAL_API}/device`)
|
.GET(`${import.meta.env.VITE_SIGNAL_API}/device`)
|
||||||
.then(res => res.json() as Promise<LocalDevice>);
|
.then(res => res.json() as Promise<LocalDevice>);
|
||||||
setLocalDevice(status);
|
setLocalDevice(status);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -678,7 +642,7 @@ export default function SettingsSidebar() {
|
||||||
<div>
|
<div>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
to={
|
to={
|
||||||
CLOUD_APP +
|
import.meta.env.VITE_CLOUD_APP +
|
||||||
"/signup?deviceId=" +
|
"/signup?deviceId=" +
|
||||||
deviceId +
|
deviceId +
|
||||||
`&returnTo=${location.href}adopt`
|
`&returnTo=${location.href}adopt`
|
||||||
|
@ -833,80 +797,6 @@ export default function SettingsSidebar() {
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
||||||
<div className="pb-2 space-y-4">
|
|
||||||
<SectionHeader
|
|
||||||
title="Hardware"
|
|
||||||
description="Configure the JetKVM Hardware"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<SettingsItem title="Display Brightness" description="Set the brightness of the display">
|
|
||||||
<SelectMenuBasic
|
|
||||||
size="SM"
|
|
||||||
label=""
|
|
||||||
value={settings.backlightSettings.max_brightness.toString()}
|
|
||||||
options={[
|
|
||||||
{ value: "0", label: "Off" },
|
|
||||||
{ value: "10", label: "Low" },
|
|
||||||
{ value: "35", label: "Medium" },
|
|
||||||
{ value: "64", label: "High" },
|
|
||||||
]}
|
|
||||||
onChange={e => {
|
|
||||||
settings.backlightSettings.max_brightness = parseInt(e.target.value)
|
|
||||||
handleBacklightSettingsChange(settings.backlightSettings);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</SettingsItem>
|
|
||||||
{settings.backlightSettings.max_brightness != 0 && (
|
|
||||||
<>
|
|
||||||
<SettingsItem title="Dim Display After" description="Set how long to wait before dimming the display">
|
|
||||||
<SelectMenuBasic
|
|
||||||
size="SM"
|
|
||||||
label=""
|
|
||||||
value={settings.backlightSettings.dim_after.toString()}
|
|
||||||
options={[
|
|
||||||
{ value: "0", label: "Never" },
|
|
||||||
{ value: "60", label: "1 Minute" },
|
|
||||||
{ value: "300", label: "5 Minutes" },
|
|
||||||
{ value: "600", label: "10 Minutes" },
|
|
||||||
{ value: "1800", label: "30 Minutes" },
|
|
||||||
{ value: "3600", label: "1 Hour" },
|
|
||||||
]}
|
|
||||||
onChange={e => {
|
|
||||||
settings.backlightSettings.dim_after = parseInt(e.target.value)
|
|
||||||
handleBacklightSettingsChange(settings.backlightSettings);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</SettingsItem>
|
|
||||||
<SettingsItem title="Turn off Display After" description="Set how long to wait before turning off the display">
|
|
||||||
<SelectMenuBasic
|
|
||||||
size="SM"
|
|
||||||
label=""
|
|
||||||
value={settings.backlightSettings.off_after.toString()}
|
|
||||||
options={[
|
|
||||||
{ value: "0", label: "Never" },
|
|
||||||
{ value: "300", label: "5 Minutes" },
|
|
||||||
{ value: "600", label: "10 Minutes" },
|
|
||||||
{ value: "1800", label: "30 Minutes" },
|
|
||||||
{ value: "3600", label: "1 Hour" },
|
|
||||||
]}
|
|
||||||
onChange={e => {
|
|
||||||
settings.backlightSettings.off_after = parseInt(e.target.value)
|
|
||||||
handleBacklightSettingsChange(settings.backlightSettings);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</SettingsItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
|
||||||
The display will wake up when the connection state changes, or when touched.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
size="SM"
|
|
||||||
theme="primary"
|
|
||||||
text="Save Display Settings"
|
|
||||||
onClick={handleBacklightSettingsSave}
|
|
||||||
/>
|
|
||||||
<div className="h-[1px] w-full bg-slate-800/10 dark:bg-slate-300/20" />
|
|
||||||
<div className="pb-2 space-y-4">
|
<div className="pb-2 space-y-4">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Advanced"
|
title="Advanced"
|
||||||
|
|
|
@ -229,12 +229,6 @@ export interface VideoState {
|
||||||
}) => void;
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BacklightSettings {
|
|
||||||
max_brightness: number;
|
|
||||||
dim_after: number;
|
|
||||||
off_after: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useVideoStore = create<VideoState>(set => ({
|
export const useVideoStore = create<VideoState>(set => ({
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
|
@ -276,9 +270,6 @@ interface SettingsState {
|
||||||
// Add new developer mode state
|
// Add new developer mode state
|
||||||
developerMode: boolean;
|
developerMode: boolean;
|
||||||
setDeveloperMode: (enabled: boolean) => void;
|
setDeveloperMode: (enabled: boolean) => void;
|
||||||
|
|
||||||
backlightSettings: BacklightSettings;
|
|
||||||
setBacklightSettings: (settings: BacklightSettings) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = create(
|
export const useSettingsStore = create(
|
||||||
|
@ -296,13 +287,6 @@ export const useSettingsStore = create(
|
||||||
// Add developer mode with default value
|
// Add developer mode with default value
|
||||||
developerMode: false,
|
developerMode: false,
|
||||||
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
setDeveloperMode: enabled => set({ developerMode: enabled }),
|
||||||
|
|
||||||
backlightSettings: {
|
|
||||||
max_brightness: 100,
|
|
||||||
dim_after: 10000,
|
|
||||||
off_after: 50000,
|
|
||||||
},
|
|
||||||
setBacklightSettings: (settings: BacklightSettings) => set({ backlightSettings: settings }),
|
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
|
|
|
@ -27,13 +27,12 @@ import LoginLocalRoute from "./routes/login-local";
|
||||||
import WelcomeLocalModeRoute from "./routes/welcome-local.mode";
|
import WelcomeLocalModeRoute from "./routes/welcome-local.mode";
|
||||||
import WelcomeRoute from "./routes/welcome-local";
|
import WelcomeRoute from "./routes/welcome-local";
|
||||||
import WelcomeLocalPasswordRoute from "./routes/welcome-local.password";
|
import WelcomeLocalPasswordRoute from "./routes/welcome-local.password";
|
||||||
import { CLOUD_API } from "./ui.config";
|
|
||||||
|
|
||||||
export const isOnDevice = import.meta.env.MODE === "device";
|
export const isOnDevice = import.meta.env.MODE === "device";
|
||||||
export const isInCloud = !isOnDevice;
|
export const isInCloud = !isOnDevice;
|
||||||
|
|
||||||
export async function checkAuth() {
|
export async function checkAuth() {
|
||||||
const res = await fetch(`${CLOUD_API}/me`, {
|
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/me`, {
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { LoaderFunctionArgs, redirect } from "react-router-dom";
|
import { LoaderFunctionArgs, redirect } from "react-router-dom";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { CLOUD_API, CLOUD_APP, SIGNAL_API } from "@/ui.config";
|
|
||||||
|
|
||||||
const loader = async ({ request }: LoaderFunctionArgs) => {
|
const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
@ -12,17 +11,17 @@ const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||||
const clientId = searchParams.get("clientId");
|
const clientId = searchParams.get("clientId");
|
||||||
|
|
||||||
const res = await api.POST(
|
const res = await api.POST(
|
||||||
`${SIGNAL_API}/cloud/register`,
|
`${import.meta.env.VITE_SIGNAL_API}/cloud/register`,
|
||||||
{
|
{
|
||||||
token: tempToken,
|
token: tempToken,
|
||||||
cloudApi: CLOUD_API,
|
cloudApi: import.meta.env.VITE_CLOUD_API,
|
||||||
oidcGoogle,
|
oidcGoogle,
|
||||||
clientId,
|
clientId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!res.ok) throw new Error("Failed to register device");
|
if (!res.ok) throw new Error("Failed to register device");
|
||||||
return redirect(CLOUD_APP + `/devices/${deviceId}/setup`);
|
return redirect(import.meta.env.VITE_CLOUD_APP + `/devices/${deviceId}/setup`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function AdoptRoute() {
|
export default function AdoptRoute() {
|
||||||
|
|
|
@ -14,7 +14,6 @@ import { User } from "@/hooks/stores";
|
||||||
import { checkAuth } from "@/main";
|
import { checkAuth } from "@/main";
|
||||||
import Fieldset from "@components/Fieldset";
|
import Fieldset from "@components/Fieldset";
|
||||||
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
|
||||||
import { CLOUD_API } from "@/ui.config";
|
|
||||||
|
|
||||||
interface LoaderData {
|
interface LoaderData {
|
||||||
device: { id: string; name: string; user: { googleId: string } };
|
device: { id: string; name: string; user: { googleId: string } };
|
||||||
|
@ -25,7 +24,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
const { deviceId } = Object.fromEntries(await request.formData());
|
const { deviceId } = Object.fromEntries(await request.formData());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${CLOUD_API}/devices/${deviceId}`, {
|
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices/${deviceId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
@ -47,7 +46,7 @@ const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${CLOUD_API}/devices/${id}`, {
|
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices/${id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { User } from "@/hooks/stores";
|
||||||
import { checkAuth } from "@/main";
|
import { checkAuth } from "@/main";
|
||||||
import Fieldset from "@components/Fieldset";
|
import Fieldset from "@components/Fieldset";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { CLOUD_API } from "@/ui.config";
|
|
||||||
|
|
||||||
interface LoaderData {
|
interface LoaderData {
|
||||||
device: { id: string; name: string; user: { googleId: string } };
|
device: { id: string; name: string; user: { googleId: string } };
|
||||||
|
@ -32,7 +31,7 @@ const action = async ({ params, request }: ActionFunctionArgs) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await api.PUT(`${CLOUD_API}/devices/${id}`, {
|
const res = await api.PUT(`${import.meta.env.VITE_CLOUD_API}/devices/${id}`, {
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
@ -50,7 +49,7 @@ const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${CLOUD_API}/devices/${id}`, {
|
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices/${id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
|
|
|
@ -16,11 +16,10 @@ import { InputFieldWithLabel } from "@components/InputField";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
import { checkAuth } from "@/main";
|
import { checkAuth } from "@/main";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { CLOUD_API } from "@/ui.config";
|
|
||||||
|
|
||||||
const loader = async ({ params }: LoaderFunctionArgs) => {
|
const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
await checkAuth();
|
await checkAuth();
|
||||||
const res = await fetch(`${CLOUD_API}/devices/${params.id}`, {
|
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices/${params.id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
@ -36,7 +35,7 @@ const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
const action = async ({ request }: ActionFunctionArgs) => {
|
const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
// Handle form submission
|
// Handle form submission
|
||||||
const { name, id, returnTo } = Object.fromEntries(await request.formData());
|
const { name, id, returnTo } = Object.fromEntries(await request.formData());
|
||||||
const res = await api.PUT(`${CLOUD_API}/devices/${id}`, { name });
|
const res = await api.PUT(`${import.meta.env.VITE_CLOUD_API}/devices/${id}`, { name });
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return redirect(returnTo?.toString() ?? `/devices/${id}`);
|
return redirect(returnTo?.toString() ?? `/devices/${id}`);
|
||||||
|
|
|
@ -36,7 +36,6 @@ import { DeviceStatus } from "./welcome-local";
|
||||||
import FocusTrap from "focus-trap-react";
|
import FocusTrap from "focus-trap-react";
|
||||||
import OtherSessionConnectedModal from "@/components/OtherSessionConnectedModal";
|
import OtherSessionConnectedModal from "@/components/OtherSessionConnectedModal";
|
||||||
import TerminalWrapper from "../components/Terminal";
|
import TerminalWrapper from "../components/Terminal";
|
||||||
import { CLOUD_API, SIGNAL_API } from "@/ui.config";
|
|
||||||
|
|
||||||
interface LocalLoaderResp {
|
interface LocalLoaderResp {
|
||||||
authMode: "password" | "noPassword" | null;
|
authMode: "password" | "noPassword" | null;
|
||||||
|
@ -57,12 +56,12 @@ export interface LocalDevice {
|
||||||
|
|
||||||
const deviceLoader = async () => {
|
const deviceLoader = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${SIGNAL_API}/device/status`)
|
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
||||||
if (!res.isSetup) return redirect("/welcome");
|
if (!res.isSetup) return redirect("/welcome");
|
||||||
|
|
||||||
const deviceRes = await api.GET(`${SIGNAL_API}/device`);
|
const deviceRes = await api.GET(`${import.meta.env.VITE_SIGNAL_API}/device`);
|
||||||
if (deviceRes.status === 401) return redirect("/login-local");
|
if (deviceRes.status === 401) return redirect("/login-local");
|
||||||
if (deviceRes.ok) {
|
if (deviceRes.ok) {
|
||||||
const device = (await deviceRes.json()) as LocalDevice;
|
const device = (await deviceRes.json()) as LocalDevice;
|
||||||
|
@ -75,11 +74,11 @@ const deviceLoader = async () => {
|
||||||
const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => {
|
const cloudLoader = async (params: Params<string>): Promise<CloudLoaderResp> => {
|
||||||
const user = await checkAuth();
|
const user = await checkAuth();
|
||||||
|
|
||||||
const iceResp = await api.POST(`${CLOUD_API}/webrtc/ice_config`);
|
const iceResp = await api.POST(`${import.meta.env.VITE_CLOUD_API}/webrtc/ice_config`);
|
||||||
const iceConfig = await iceResp.json();
|
const iceConfig = await iceResp.json();
|
||||||
|
|
||||||
const deviceResp = await api.GET(
|
const deviceResp = await api.GET(
|
||||||
`${CLOUD_API}/devices/${params.id}`,
|
`${import.meta.env.VITE_CLOUD_API}/devices/${params.id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!deviceResp.ok) {
|
if (!deviceResp.ok) {
|
||||||
|
@ -143,7 +142,7 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sd = btoa(JSON.stringify(pc.localDescription));
|
const sd = btoa(JSON.stringify(pc.localDescription));
|
||||||
const res = await api.POST(`${SIGNAL_API}/webrtc/session`, {
|
const res = await api.POST(`${import.meta.env.VITE_SIGNAL_API}/webrtc/session`, {
|
||||||
sd,
|
sd,
|
||||||
// When on device, we don't need to specify the device id, as it's already known
|
// When on device, we don't need to specify the device id, as it's already known
|
||||||
...(isOnDevice ? {} : { id: params.id }),
|
...(isOnDevice ? {} : { id: params.id }),
|
||||||
|
@ -318,7 +317,7 @@ export default function KvmIdRoute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire and forget
|
// Fire and forget
|
||||||
api.POST(`${CLOUD_API}/webrtc/turn_activity`, {
|
api.POST(`${import.meta.env.VITE_CLOUD_API}/webrtc/turn_activity`, {
|
||||||
bytesReceived: bytesReceivedDelta,
|
bytesReceived: bytesReceivedDelta,
|
||||||
bytesSent: bytesSentDelta,
|
bytesSent: bytesSentDelta,
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { User } from "@/hooks/stores";
|
||||||
import EmptyCard from "@components/EmptyCard";
|
import EmptyCard from "@components/EmptyCard";
|
||||||
import { LuMonitorSmartphone } from "react-icons/lu";
|
import { LuMonitorSmartphone } from "react-icons/lu";
|
||||||
import { ArrowRightIcon } from "@heroicons/react/16/solid";
|
import { ArrowRightIcon } from "@heroicons/react/16/solid";
|
||||||
import { CLOUD_API } from "@/ui.config";
|
|
||||||
|
|
||||||
interface LoaderData {
|
interface LoaderData {
|
||||||
devices: { id: string; name: string; online: boolean; lastSeen: string }[];
|
devices: { id: string; name: string; online: boolean; lastSeen: string }[];
|
||||||
|
@ -20,7 +19,7 @@ export const loader = async () => {
|
||||||
const user = await checkAuth();
|
const user = await checkAuth();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${CLOUD_API}/devices`, {
|
const res = await fetch(`${import.meta.env.VITE_CLOUD_API}/devices`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
|
|
|
@ -12,16 +12,15 @@ import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
import ExtLink from "../components/ExtLink";
|
import ExtLink from "../components/ExtLink";
|
||||||
import { SIGNAL_API } from "@/ui.config";
|
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${SIGNAL_API}/device/status`)
|
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
||||||
if (!res.isSetup) return redirect("/welcome");
|
if (!res.isSetup) return redirect("/welcome");
|
||||||
|
|
||||||
const deviceRes = await api.GET(`${SIGNAL_API}/device`);
|
const deviceRes = await api.GET(`${import.meta.env.VITE_SIGNAL_API}/device`);
|
||||||
if (deviceRes.ok) return redirect("/");
|
if (deviceRes.ok) return redirect("/");
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
@ -32,7 +31,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.POST(
|
const response = await api.POST(
|
||||||
`${SIGNAL_API}/auth/login-local`,
|
`${import.meta.env.VITE_SIGNAL_API}/auth/login-local`,
|
||||||
{
|
{
|
||||||
password,
|
password,
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,11 +9,10 @@ import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||||
import { cx } from "../cva.config";
|
import { cx } from "../cva.config";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
import { SIGNAL_API } from "@/ui.config";
|
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${SIGNAL_API}/device/status`)
|
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
||||||
if (res.isSetup) return redirect("/login-local");
|
if (res.isSetup) return redirect("/login-local");
|
||||||
|
@ -31,7 +30,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
|
|
||||||
if (localAuthMode === "noPassword") {
|
if (localAuthMode === "noPassword") {
|
||||||
try {
|
try {
|
||||||
await api.POST(`${SIGNAL_API}/device/setup`, {
|
await api.POST(`${import.meta.env.VITE_SIGNAL_API}/device/setup`, {
|
||||||
localAuthMode,
|
localAuthMode,
|
||||||
});
|
});
|
||||||
return redirect("/");
|
return redirect("/");
|
||||||
|
|
|
@ -10,11 +10,10 @@ import LogoBlueIcon from "@/assets/logo-blue.png";
|
||||||
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { DeviceStatus } from "./welcome-local";
|
import { DeviceStatus } from "./welcome-local";
|
||||||
import { SIGNAL_API } from "@/ui.config";
|
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${SIGNAL_API}/device/status`)
|
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
||||||
if (res.isSetup) return redirect("/login-local");
|
if (res.isSetup) return redirect("/login-local");
|
||||||
|
@ -31,7 +30,7 @@ const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.POST(`${SIGNAL_API}/device/setup`, {
|
const response = await api.POST(`${import.meta.env.VITE_SIGNAL_API}/device/setup`, {
|
||||||
localAuthMode: "password",
|
localAuthMode: "password",
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,6 @@ import LogoMark from "@/assets/logo-mark.png";
|
||||||
import { cx } from "cva";
|
import { cx } from "cva";
|
||||||
import api from "../api";
|
import api from "../api";
|
||||||
import { redirect } from "react-router-dom";
|
import { redirect } from "react-router-dom";
|
||||||
import { SIGNAL_API } from "@/ui.config";
|
|
||||||
|
|
||||||
export interface DeviceStatus {
|
export interface DeviceStatus {
|
||||||
isSetup: boolean;
|
isSetup: boolean;
|
||||||
|
@ -17,7 +16,7 @@ export interface DeviceStatus {
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async () => {
|
||||||
const res = await api
|
const res = await api
|
||||||
.GET(`${SIGNAL_API}/device/status`)
|
.GET(`${import.meta.env.VITE_SIGNAL_API}/device/status`)
|
||||||
.then(res => res.json() as Promise<DeviceStatus>);
|
.then(res => res.json() as Promise<DeviceStatus>);
|
||||||
|
|
||||||
if (res.isSetup) return redirect("/login-local");
|
if (res.isSetup) return redirect("/login-local");
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
interface JetKVMConfig {
|
|
||||||
CLOUD_API?: string;
|
|
||||||
CLOUD_APP?: string;
|
|
||||||
DEVICE_VERSION?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window { JETKVM_CONFIG?: JetKVMConfig; }
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAppURL = (api_url?: string) => {
|
|
||||||
if (!api_url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = new URL(api_url);
|
|
||||||
url.host = url.host.replace(/api\./, "app.");
|
|
||||||
// remove the ending slash
|
|
||||||
return url.toString().replace(/\/$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CLOUD_API = window.JETKVM_CONFIG?.CLOUD_API || import.meta.env.VITE_CLOUD_API;
|
|
||||||
export const CLOUD_APP = window.JETKVM_CONFIG?.CLOUD_APP || getAppURL(CLOUD_API) || import.meta.env.VITE_CLOUD_APP;
|
|
||||||
export const SIGNAL_API = import.meta.env.VITE_SIGNAL_API;
|
|
22
web.go
22
web.go
|
@ -2,8 +2,6 @@ package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -79,9 +77,6 @@ func setupRouter() *gin.Engine {
|
||||||
// We use this to determine if the device is setup
|
// We use this to determine if the device is setup
|
||||||
r.GET("/device/status", handleDeviceStatus)
|
r.GET("/device/status", handleDeviceStatus)
|
||||||
|
|
||||||
// We use this to provide the UI with the device configuration
|
|
||||||
r.GET("/device/ui-config.js", handleDeviceUIConfig)
|
|
||||||
|
|
||||||
// We use this to setup the device in the welcome page
|
// We use this to setup the device in the welcome page
|
||||||
r.POST("/device/setup", handleSetup)
|
r.POST("/device/setup", handleSetup)
|
||||||
|
|
||||||
|
@ -366,23 +361,6 @@ func handleDeviceStatus(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, response)
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDeviceUIConfig(c *gin.Context) {
|
|
||||||
LoadConfig()
|
|
||||||
|
|
||||||
config, _ := json.Marshal(gin.H{
|
|
||||||
"CLOUD_API": config.CloudURL,
|
|
||||||
"DEVICE_VERSION": builtAppVersion,
|
|
||||||
})
|
|
||||||
if config == nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to marshal config"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
response := fmt.Sprintf("window.JETKVM_CONFIG = %s;", config)
|
|
||||||
|
|
||||||
c.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte(response))
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSetup(c *gin.Context) {
|
func handleSetup(c *gin.Context) {
|
||||||
LoadConfig()
|
LoadConfig()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue